RunLoop — iOS-এ ইভেন্ট প্রক্রিয়াকরণ লুপ, CFRunLoop (Core Foundation) এবং NSRunLoop (Foundation) অবজেক্ট দ্বারা বাস্তবায়িত। এটি একটি মেকানিজম যা ইভেন্টের জন্য অপেক্ষা করে (স্পর্শ, টাইমার, ইনপুট সোর্স, বিজ্ঞপ্তি) এবং থ্রেডে সংশ্লিষ্ট হ্যান্ডলারদের কাছে প্রেরণ করে। iOS-এর প্রতিটি থ্রেডে সর্বোচ্চ একটি RunLoop থাকে, কিন্তু এটি শুধুমাত্র Main Thread-এর জন্য স্বয়ংক্রিয়ভাবে তৈরি হয়। Apple CFRunLoop Documentation অনুসারে, ব্যাকগ্রাউন্ড থ্রেডে টাইমার, অ্যানিমেশন এবং সোর্স মনিটরিংয়ের জন্য RunLoop অপরিহার্য।
মুখ্য বিষয়
RunLoop — একটি Core Foundation ইনফ্রাস্ট্রাকচার অবজেক্ট যা থ্রেডে ইভেন্ট প্রক্রিয়াকরণ সংগঠিত করে। মূলত এটি একটি অসীম লুপ (while true) যা ইভেন্টের আগমনের জন্য অপেক্ষা করে (sources) এবং হ্যান্ডলারদের কাছে প্রেরণ করে। যখন কোনো ইভেন্ট থাকে না, RunLoop থ্রেডকে স্লিপ মোডে নিয়ে যায়, ব্যাটারির শক্তি সাশ্রয় করে। ইভেন্ট আসলে থ্রেড জেগে ওঠে, এটি প্রক্রিয়া করে এবং আবার ঘুমিয়ে পড়ে। RunLoop শুধুমাত্র iOS/macOS (XNU + Core Foundation)-এ বিদ্যমান — Android-এ এর ভূমিকা পালন করে Looper।
প্রতিটি থ্রেডে সর্বোচ্চ একটি RunLoop থাকে, যা প্রথম অনুরোধে লেজি (lazy) পদ্ধতিতে তৈরি হয়। Main Thread-এর জন্য RunLoop অ্যাপ্লিকেশন চালু হলে স্বয়ংক্রিয়ভাবে তৈরি হয়। ব্যাকগ্রাউন্ড থ্রেডের জন্য RunLoop তৈরি হয় না যতক্ষণ না CFRunLoopGetCurrent() বা RunLoop.current কল করা হয়। প্রধান RunLoop টাচ-ইভেন্ট, স্ক্রিন রেন্ডারিং, DispatchQueue.main ব্লক নির্বাহ এবং Core Animation লেয়ার সার্ভিসিং-এর জন্য দায়ী।
RunLoop থ্রেড নয় — এটি থ্রেডের ভিতরে একটি মেকানিজম। থ্রেড RunLoop ছাড়া থাকতে পারে (যদি সিনক্রোনাস কাজ সম্পাদন করে এবং শেষ হয়), কিন্তু RunLoop থ্রেড ছাড়া থাকতে পারে না। যখন সক্রিয় RunLoop-সহ থ্রেডে কোনো ইভেন্ট থাকে না, এটি CPU ব্লক করে না, বরং অপেক্ষারত (waiting) অবস্থায় থাকে — এটি busy-wait লুপ থেকে মূল পার্থক্য, যা 100% CPU ব্যবহার করে।
RunLoop দুই ধরনের ইভেন্ট সোর্স প্রক্রিয়া করে: Input Sources (ইনপুট সোর্স) এবং Timer Sources (টাইমার)। Input Sources অ্যাসিনক্রোনাস ইভেন্ট সরবরাহ করে: স্পর্শ, মাউস মুভমেন্ট, সকেট ডেটা, অন্য থ্রেড থেকে বার্তা (performSelector:onThread:)। Timer Sources নির্ধারিত সময় অনুযায়ী সিনক্রোনাস ইভেন্ট সরবরাহ করে: NSTimer, CADisplayLink। এছাড়াও Observers রয়েছে — RunLoop অবস্থা পর্যবেক্ষণের জন্য এন্ট্রি পয়েন্ট।
RunLoop লুপটি ধারাবাহিক পর্যায় নিয়ে গঠিত: মোডে প্রবেশ (kCFRunLoopEntry), টাইমার প্রক্রিয়াকরণ (kCFRunLoopBeforeTimers), ইনপুট সোর্স প্রক্রিয়াকরণ (kCFRunLoopBeforeSources), সোর্স প্রক্রিয়াকরণ (kCFRunLoopAfterWaiting), অপেক্ষা (sleep), মোড থেকে প্রস্থান (kCFRunLoopExit)। যদি বর্তমান ইটারেশনে কোনো ইভেন্ট প্রক্রিয়াকৃত না হয়, RunLoop থ্রেডকে অনির্দিষ্ট সময়ের জন্য স্লিপে পাঠায় যতক্ষণ না নতুন ইভেন্ট জাগিয়ে তোলে।
import Foundation
// Observer-এর মাধ্যমে RunLoop পর্যায় প্রদর্শন
func observeRunLoopActivities() {
let observer = CFRunLoopObserverCreateWithHandler(
nil,
CFOptionFlags([[.entry, .beforeTimers, .beforeSources,
.afterWaiting, .exit]]),
true, // repeats
0 // priority
) { observer, activity in
switch activity {
case .entry:
print("Entry — RunLoop সক্রিয় হয়েছে")
case .beforeTimers:
print("BeforeTimers — টাইমার প্রক্রিয়াকরণ")
case .beforeSources:
print("BeforeSources — সোর্স প্রক্রিয়াকরণ")
case .afterWaiting:
print("AfterWaiting — sleep থেকে জাগরণ")
case .exit:
print("Exit — RunLoop সম্পন্ন হয়েছে")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// উদাহরণ: RunLoop প্রধান থ্রেডে টাইমার প্রক্রিয়া করে
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// RunLoop.current.run() Main Thread-এ UIApplicationMain দ্বারা কল হয়
// স্বয়ংক্রিয়ভাবে — ম্যানুয়ালি চালু করার প্রয়োজন নেই
RunLoop.current.run() // এই কল Main Thread-এ ফিরে আসবে না
}
উদাহরণ observeRunLoopActivities প্রধান RunLoop-এ একটি Observer নিবন্ধন করে যা লুপের প্রতিটি পর্যায় লগ করে। এটি ডিবাগিংয়ের জন্য উপযোগী: যদি আপনি .beforeTimers এবং .afterWaiting-এর মধ্যে দীর্ঘ ব্যবধান দেখেন, তাহলে Main Thread-এ কোনো অপারেশন RunLoop ব্লক করছে। timerOnMainRunLoop দেখায় কিভাবে NSTimer স্বয়ংক্রিয়ভাবে প্রধান RunLoop-এ কাজ করে — Timer.scheduledTimer তৈরি করার সময় ডিফল্টভাবে বর্তমান RunLoop-এ (.default mode) টাইমার যোগ করে।
Android Looper — RunLoop-এর অনুরূপ। Looper.prepare() থ্রেডে একটি মেসেজ কিউ (MessageQueue) তৈরি করে, Looper.loop() অসীম প্রক্রিয়াকরণ লুপ চালু করে। Handler মেসেজ এবং Runnable এই কিউতে পাঠায়। প্রধান পার্থক্য: RunLoop মোড (modes) সমর্থন করে, কিন্তু Android Looper সমর্থন করে না। Looper মোড অনুযায়ী ফিল্টারিং ছাড়াই সব মেসেজ প্রক্রিয়া করে, যা এটিকে সহজ কিন্তু অগ্রাধিকারভিত্তিক পরিস্থিতির জন্য কম নমনীয় করে তোলে (যেমন iOS-এ স্ক্রল .tracking mode-এ অন্যদের থেকে আলাদাভাবে প্রক্রিয়াকৃত হয়)।
RunLoop Mode — এটি সোর্স, টাইমার এবং পর্যবেক্ষকদের একটি সেট যা বর্তমানে সক্রিয়। মোডগুলি অগ্রাধিকার অনুযায়ী ইভেন্ট প্রক্রিয়াকরণকে আলাদা করতে দেয়। যখন ব্যবহারকারী UITableView স্ক্রল করে, RunLoop .tracking মোডে সুইচ করে, যেখানে শুধুমাত্র স্ক্রল ইভেন্ট এবং সংশ্লিষ্ট টাইমার/অ্যানিমেশন প্রক্রিয়াকৃত হয়। অন্য সব সোর্স (যেমন NSURLConnection) স্ক্রল মোড থেকে বের হওয়া পর্যন্ত স্থগিত থাকে।
তিনটি প্রধান মোড: .default (NSDefaultRunLoopMode) — প্রধান মোড, যেখানে স্ক্রল ছাড়া সব ইভেন্ট প্রক্রিয়াকৃত হয়; .tracking (UITrackingRunLoopMode) — স্ক্রল বা জেসচার নেভিগেশনের সময় সক্রিয় হয়; .common (NSRunLoopCommonModes) — আলাদা মোড নয়, বরং একটি别名 সেট যা .default + .tracking অন্তর্ভুক্ত করে। .commonModes-এ একটি সোর্স যোগ করলে তা সেটের সব মোডে স্বয়ংক্রিয়ভাবে যোগ হয়।
| মোড | Core Foundation কনস্ট্যান্ট | Foundation কনস্ট্যান্ট | কখন সক্রিয় |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | সাধারণ অবস্থা, স্ক্রল ছাড়া |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | স্ক্রল, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | সিউডো-মোড: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | RunLoop-এর প্রথম চালু |
শাস্ত্রীয় সমস্যা: NSTimer, যা .default mode-এ যোগ করা হয়েছে, স্ক্রলের সময় কাজ করা বন্ধ করে দেয় কারণ RunLoop .tracking mode-এ সুইচ করে এবং .default-এর টাইমার প্রক্রিয়া করে না। সমাধান — টাইমার .commonModes-এ যোগ করুন: RunLoop.current.add(timer, forMode: .common)। এতে Timer .default এবং .tracking উভয়েই কাজ করবে। বিকল্প — NSTimer-এর পরিবর্তে DispatchQueue.main.async ব্যবহার করুন, কারণ GCD থ্রেড স্তরে কাজ করে, RunLoop মোডে নয়।
ব্যাকগ্রাউন্ড থ্রেড-এ ডিফল্টভাবে RunLoop থাকে না। যদি ব্যাকগ্রাউন্ড থ্রেডে NSTimer চালানো, NSInputStream/NSOutputStream প্রক্রিয়াকরণ বা performSelector:-এ সাড়া দেওয়ার প্রয়োজন হয়, তাহলে ম্যানুয়ালি RunLoop তৈরি এবং চালু করতে হবে। RunLoop ছাড়া টাইমার এবং performSelector: কখনো কাজ করবে না — থ্রেড কোড চালিয়ে শেষ হয়ে যাবে, ইভেন্টের অপেক্ষা না করে।
ব্যাকগ্রাউন্ড থ্রেডে RunLoop তৈরি করতে থ্রেডের শেষে RunLoop.current.run() কল করাই যথেষ্ট। এই কল থ্রেডকে অসীমভাবে ব্লক করে, ইভেন্ট প্রক্রিয়া করে। বন্ধ করতে CFRunLoopStop(CFRunLoopGetCurrent()) ব্যবহার করুন। গুরুত্বপূর্ণ: RunLoop.current প্রথম অনুরোধে লেজি পদ্ধতিতে RunLoop তৈরি করে — যদি run() কল না করা হয়, এটি ইভেন্ট প্রক্রিয়া করবে না। প্যাটার্ন: সোর্স কনফিগারেশন → RunLoop-এ যোগ → run() কল।
import Foundation
// নিজস্ব RunLoop-সহ ব্যাকগ্রাউন্ড থ্রেড
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop.current কল করলে RunLoop স্বয়ংক্রিয়ভাবে তৈরি হয়
let runLoop = RunLoop.current
// RunLoop সক্রিয় রাখতে একটি পোর্ট যোগ করি
runLoop.add(Port(), forMode: .default)
// ইভেন্ট প্রক্রিয়াকরণ শুরু করি
var isFinished = false
while !isFinished {
// run(mode:before:) true রিটার্ন করে যদি ইভেন্ট প্রক্রিয়াকৃত হয়
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// ব্যাকগ্রাউন্ড থ্রেডে RunLoop বন্ধ করা
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// ব্যবহার: ব্যাকগ্রাউন্ড RunLoop-এ টাইমার
let manager = BackgroundRunLoopManager()
manager.start()
// performSelector-এর মাধ্যমে ব্যাকগ্রাউন্ড RunLoop-এ টাস্ক পাঠানো
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager একটি ধ্রুবক RunLoop-সহ ব্যাকগ্রাউন্ড থ্রেড তৈরি করে। খালি Port() যোগ করা প্রয়োজন যাতে RunLoop তৎক্ষণাৎ শেষ না হয় — সোর্স ছাড়া RunLoop.run() false রিটার্ন করে এবং বেরিয়ে যায়। performSelector:onThread: ব্যাকগ্রাউন্ড RunLoop-এ একটি বার্তা পাঠায় — এটি প্রক্রিয়াকৃত হবে যখন RunLoop BeforeSources পর্যায়ে প্রবেশ করবে। Stop ব্যাকগ্রাউন্ড থ্রেডে CFRunLoopStop কল করে, লুপ শেষ করে।
NSTimer একটি টাইমার-ইভেন্ট তৈরি করে যা RunLoop BeforeTimers পর্যায়ে প্রক্রিয়া করে। টাইমারগুলি repeating (পুনরাবৃত্ত) এবং non-repeating (একবার) হতে পারে। NSTimer নির্ভুলতা গ্যারান্টি দেয় না: যদি RunLoop দীর্ঘ অপারেশনে ব্লক থাকে, টাইমার আনব্লকের পরে সক্রিয় হবে, এবং সব মিস করা সক্রিয়তা একটিতে একত্রিত হবে (repeating টাইমারের জন্য সর্বোচ্চ একটি “ক্যাচ-আপ” সক্রিয়তা)।
CADisplayLink — একটি বিশেষায়িত টাইমার, স্ক্রিন রিফ্রেশ রেটের (60/120/144 Hz) সাথে সিঙ্ক্রোনাইজড। এটি অ্যানিমেশন এবং ভিডিও আপডেটের জন্য ব্যবহৃত হয়। CADisplayLink RunLoop-এ যোগ করা হয় এবং প্রতিটি রেন্ডারিং ফ্রেমের আগে সক্রিয় হয় (Core Animation লেয়ার রেন্ডারিংয়ে পাঠানোর আগে)। যদি ফ্রেম মিস হয় (display link 16 ms-এ সক্রিয় হতে না পারে), পরবর্তী কল পরবর্তী VSync চক্রে ঘটে।
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — vsync-সহ অ্যানিমেশন
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// .common মোডে যোগ — স্ক্রলের সময়ও কাজ করে
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// প্রতি ফ্রেমে কল হয় (60 FPS → প্রতি 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // 5 সেকেন্ড পরে বন্ধ
}
}
// NSTimer — পর্যায়ক্রমিক কাজ
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// মুখ্য: .common-এ যোগ করুন, অন্যথায় টাইমার স্ক্রলে জমে যাবে
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
AnimationController-এ CADisplayLink .common mode-এ যোগ করা হয়েছে, যা স্ক্রল নির্বিশেষে প্রতিটি ফ্রেমে step কল নিশ্চিত করে। displayLink.add(to: .current, forMode: .common) — অ্যানিমেশনের জন্য আদর্শ প্যাটার্ন যা স্ক্রল করার সময় বাধাগ্রস্ত হওয়া উচিত নয়। NSTimer-ও .common mode-এ যোগ করা হয়েছে যাতে স্ক্রলের সময় টিক করতে পারে। এটি ছাড়া টাইমার শুধুমাত্র .default mode-এ কাজ করত।
CFRunLoopObserver — RunLoop পর্যায় ট্র্যাক করার জন্য মেকানিজম। Observer-এর মাধ্যমে মোডে প্রবেশ, টাইমার প্রক্রিয়াকরণ শুরু, সোর্স প্রক্রিয়াকরণ শুরু, ঘুম থেকে জাগরণ, মোড থেকে প্রস্থান সম্পর্কে বিজ্ঞপ্তি পাওয়া যায়। Observer ফ্রেমওয়ার্কগুলি নিজস্ব প্রয়োজনে ব্যবহার করে: Core Animation এগুলি RunLoop ঘুমানোর আগে লেয়ার রেন্ডারিংয়ের জন্য ব্যবহার করে, UIKit — ইভেন্ট প্রক্রিয়াকরণের পর layout আপডেটের জন্য।
ডেভেলপারও নিজস্ব উদ্দেশ্যে Observer যোগ করতে পারেন। যেমন: ইভেন্ট প্রক্রিয়াকরণ সময় পরিমাপ (প্রোফাইলিং), RunLoop ঘুমানোর আগে বিলম্বিত অপারেশন সম্পাদন (যখন UI ইতিমধ্যে আপডেট এবং ব্যবহারকারী ইন্টারঅ্যাক্ট করছে না), দীর্ঘ নিষ্ক্রিয়তায় স্বয়ংক্রিয় ডেটা সংরক্ষণ। Observer CFRunLoopAddObserver-এর মাধ্যমে মোড এবং পর্যবেক্ষণযোগ্য অ্যাক্টিভিটির বিট মাস্ক উল্লেখ করে নিবন্ধিত হয়।
Observer-এর জন্য সবচেয়ে উপযোগী পয়েন্টগুলি: .afterWaiting — RunLoop জাগরণের পরে কার্যকর হয় এবং ইভেন্ট প্রক্রিয়াকরণের পরে চালানো উচিত এমন কোড থাকতে পারে; .beforeTimers — টাইমার প্রক্রিয়াকরণের আগে, পূর্ববর্তী প্রক্রিয়াকরণ থেকে সময় পরিমাপ করতে দেয়; .exit — RunLoop বন্ধ হলে সক্রিয় হয়, ব্যাকগ্রাউন্ড থ্রেডের রিসোর্স ক্লিনআপের জন্য উপযোগী।
CFRunLoopStop — ফাংশন যা বর্তমান RunLoop ইটারেশন জোরপূর্বক সমাপ্ত করে। CFRunLoopStop(CFRunLoopGetCurrent()) কল করলে RunLoop বর্তমান ইভেন্ট প্রক্রিয়াকরণ শেষ করে এবং run() থেকে বেরিয়ে false রিটার্ন করে। এটি ব্যাকগ্রাউন্ড থ্রেডে RunLoop বন্ধ করার আদর্শ উপায়। Main Thread-এ CFRunLoopStop সুপারিশ করা হয় না — প্রধান RunLoop অ্যাপ্লিকেশনের জীবনকাল জুড়ে কাজ করা উচিত। ব্যাকগ্রাউন্ড থ্রেডের জন্য CFRunLoopStop-এর পর থ্রেড শেষ হতে পারে বা run()-এর পরবর্তী কোড নির্বাহ চালিয়ে যেতে পারে।
প্রায়শই জিজ্ঞাসিত প্রশ্ন
RunLoop — iOS-এ ইভেন্ট প্রক্রিয়াকরণ লুপ, CFRunLoop (Core Foundation) এবং NSRunLoop (Foundation) দ্বারা বাস্তবায়িত। এটি ইভেন্টের জন্য অপেক্ষা করে (স্পর্শ, টাইমার, ইনপুট সোর্স) এবং থ্রেডে হ্যান্ডলারদের কাছে প্রেরণ করে। প্রতিটি থ্রেডে একটি RunLoop থাকতে পারে, কিন্তু স্বয়ংক্রিয়ভাবে শুধুমাত্র Main Thread-এর জন্য তৈরি হয়। RunLoop মোড (.default, .tracking, .common) পরিচালনা করে, অগ্রাধিকার অনুযায়ী প্রক্রিয়াকরণ আলাদা করে।
NSTimer ডিফল্টভাবে .default RunLoop মোডে যোগ করা হয়। যখন ব্যবহারকারী স্ক্রল করে, RunLoop .tracking মোডে সুইচ করে এবং .default-এর টাইমার প্রক্রিয়া করে না। সমাধান: টাইমার .common মোডে যোগ করুন RunLoop.current.add(timer, forMode: .common)-এর মাধ্যমে। .common .default এবং .tracking একত্রিত করে, তাই টাইমার উভয় মোডেই কাজ করে।
শুধুমাত্র যদি ব্যাকগ্রাউন্ড থ্রেড টাইমার (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream বা Source-ইভেন্ট ব্যবহার করে। যদি থ্রেড সিনক্রোনাস কাজ সম্পাদন করে (ফাইল ডাউনলোড, গণনা) এবং শেষ হয় — RunLoop প্রয়োজন নেই। চালু করতে সোর্স কনফিগারেশনের পর RunLoop.current.run() কল করুন। বন্ধ করতে — CFRunLoopStop(CFRunLoopGetCurrent())।
RunLoop থ্রেড স্তরে কাজ করে এবং মোড (modes) সমর্থন সহ ধারাবাহিকভাবে ইভেন্ট প্রক্রিয়া করে। DispatchQueue — থ্রেড পুলের একটি অ্যাবস্ট্রাকশন, কাজ যেকোনো মুক্ত থ্রেডে সম্পাদিত হয়। GCD মোড সমর্থন করে না এবং RunLoop থেকে স্বাধীন। DispatchQueue.main ব্লক নির্বাহের জন্য প্রধান RunLoop ব্যবহার করে — এটি একমাত্র সংযোগ বিন্দু। ব্যাকগ্রাউন্ড কাজের জন্য GCD পছন্দনীয়।
CADisplayLink — একটি টাইমার যা VSync-এর (স্ক্রিন রিফ্রেশ রেট) সাথে সিঙ্ক্রোনাইজড। এটি RunLoop-এ যোগ করা হয় এবং BeforeTimers পর্যায়ে প্রতিটি ফ্রেমের আগে সক্রিয় হয়। CADisplayLink শুধুমাত্র Main Thread-এ কাজ করে, কারণ স্ক্রিন রেন্ডারিং সেখানেই ঘটে। স্ক্রলের সময় নিরবচ্ছিন্ন অ্যানিমেশনের জন্য এটি .common মোডে যোগ করুন: displayLink.add(to: .current, forMode: .common)।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন