Main Thread — نخ اصلی اجرا در برنامههای موبایل است که کل رابط کاربری را پردازش میکند: لمسها، رندر، بهروزرسانی layout و انیمیشنها. در iOS این RunLoop.main است، در Android — Looper.getMainLooper(). هر عملیات طولانی روی این نخ UI را مسدود کرده و باعث ANR (Android) یا هنگ کردن رابط (iOS) میشود. طبق Apple UIKit Documentation، کلاسهای UI ایمنی نخ ندارند و نیاز به فراخوانی منحصراً از Main Thread دارند.
نکات اصلی
Main Thread — نخی است که توسط سیستم عامل هنگام راهاندازی برنامه ایجاد میشود و مسئول پردازش همه رویدادهای رابط کاربری است. در زمینه پلتفرمهای موبایل، Main Thread همچنین UI Thread نامیده میشود، زیرا تمام عملیات مربوط به رندر، پردازش لمس و انیمیشن روی آن اجرا میشود. هر برنامه دقیقاً یک Main Thread دارد و همه فریمورکهای UI (UIKit, AppKit, Android Views, Compose UI) thread-unsafe هستند — آنها عملکرد صحیح را هنگام فراخوانی از نخهای دیگر تضمین نمیکنند.
از نظر معماری، Main Thread الگوی Event Loop را پیادهسازی میکند: نخ به طور نامحدود منتظر رویدادهای جدید (لمس، اعلانهای سیستم، تایمرها) میماند و آنها را به ترتیب صف پردازش میکند. در حین پردازش یک رویداد، رویداد بعدی در صف منتظر میماند. اگر پردازش بیش از ۱۰۰-۲۰۰ میلیثانیه طول بکشد، کاربر تأخیر (jank) را متوجه میشود. اگر بیش از ۵ ثانیه (Android) — سیستم دیالوگ ANR (Application Not Responding) را نشان میدهد و پیشنهاد بستن برنامه را میدهد.
اهمیت درک Main Thread دشوار است: این منبع ۹۰٪ مشکلات عملکرد در برنامههای موبایل است. توسعهدهندگان اغلب فراموش میکنند عملیات سنگین (شبکه، فایلها، تجزیه JSON، فشردهسازی تصاویر) را به نخهای پسزمینه منتقل کنند. حتی عملیاتی که روی شبیهساز در ۱۰ میلیثانیه اجرا میشود، روی دستگاه واقعی با دیسک کند ممکن است ۵۰۰ میلیثانیه طول بکشد و منجر به لگ قابل توجهی شود.
Thread-unsafe فریمورکهای UI — تصمیم معماری گرفته شده در اولین نسخههای UIKit (۲۰۰۷) و Android (۲۰۰۸). دلیل اصلی عملکرد است: همگامسازی دسترسی به کامپوننتهای UI از طریق قفلها (locks) سربار اضافی به هر عملیات رندر اضافه میکرد. در عوض، فریمورکها نیاز دارند که تمام تغییرات UI به طور دقیق روی یک نخ انجام شود، وضعیت مسابقه (race condition) را بدون overhead حذف میکند.
تصور کنید دو نخ پسزمینه به طور همزمان textView.setText() را فراخوانی میکنند. اگر UI thread-safe بود، هر دو فراخوانی از طریق mutex همگامسازی میشدند که رندر را ۲۰-۴۰٪ کند میکرد. در معماری فعلی، هر فراخوانی UI از نخ پسزمینه یا نادیده گرفته میشود یا باعث crash میشود (در iOS — Main Thread Checker Exception، در Android — CalledFromWrongThreadException). استثنا — SurfaceView و TextureView در Android، جایی که رندر میتواند از یک نخ جداگانه انجام شود.
فریمورکهای مدرن موبایل (SwiftUI, Jetpack Compose) این محدودیت را حفظ میکنند: SwiftUI نیاز دارد که تمام تغییرات State و ObservedObject در Main Thread انجام شود، اگرچه خود رندر تا حدی به نخهای پسزمینه منتقل شده است. Jetpack Compose نیز انتظار تغییر State را در Main Thread دارد. استثنا — اصلاحکنندههای Compose مرتبط با drawBehind و layout که میتوانند با مستندات صریح از نخهای دیگر فراخوانی شوند.
DispatchQueue.main — مکانیسم اصلی ارسال کد به Main Thread در iOS. این یک صف سریال است که به RunLoop اصلی برنامه متصل است. تمام بلوکهای ارسال شده به آن به ترتیب، به ترتیب ورود اجرا میشوند. SwiftUI و UIKit به طور خودکار بهروزرسانی میشوند اگر State را تغییر دهید یا setNeedsLayout() را از Main Thread فراخوانی کنید. برای بازگشت ناهمگام نتیجه از یک کار پسزمینه از DispatchQueue.main.async {} استفاده کنید.
در پل Objective-C-Swift همچنین Thread.isMainThread در دسترس است — ویژگی که بررسی میکند آیا کد فعلی روی نخ اصلی اجرا میشود یا خیر. برای پروژههای موجود UIKit این یک الگوی استاندارد است: if Thread.isMainThread { updateUI() } else { DispatchQueue.main.async { updateUI() } }. در SwiftUI این بررسی معمولاً لازم نیست، زیرا فریمورک خود تضمین میکند که body و modifier در Main Thread فراخوانی میشوند.
import UIKit
class ViewController: UIViewController {
let imageView = UIImageView()
func loadImageFromNetwork() {
// نخ پسزمینه: دانلود تصویر
DispatchQueue.global(qos: .background).async { [weak self] in
guard let url = URL(string: "https://example.com/image.png"),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data)
else { return }
// بازگشت به Main Thread برای بهروزرسانی UI
DispatchQueue.main.async {
self?.imageView.image = image
self?.imageView.setNeedsLayout()
}
}
}
// بررسی اینکه آیا کد در Main Thread اجرا میشود
func safeUpdateUI() {
if Thread.isMainThread {
updateUI()
} else {
DispatchQueue.main.async {
self.updateUI()
}
}
}
private func updateUI() {
print("UI در Main Thread بهروزرسانی شد")
}
}
مثال loadImageFromNetwork() الگوی صحیح را نشان میدهد: URLSession یا Data(contentsOf:) از طریق DispatchQueue.global روی نخ پسزمینه اجرا میشوند، پس از آن نتیجه برای بهروزرسانی UIImageView به DispatchQueue.main بازگردانده میشود. بدون DispatchQueue.main.async برنامه با NSInternalInconsistencyException هنگام فراخوانی UIKit از نخ پسزمینه crash میکند.
مطمئنترین راه اجرای کد در Main Thread در iOS — ارسال صریح از طریق DispatchQueue.main.async. حتی اگر قبلاً در Main Thread هستید، ارسال async مشکلی ایجاد نمیکند: GCD آن را در تکرار بعدی RunLoop پردازش میکند. برای اجرای همگام از DispatchQueue.main.sync استفاده کنید، اما این ممکن است باعث deadlock شود اگر sync را از Main Thread فراخوانی کنید. قانون: async برای بازگشت نتیجه، sync فقط اگر تضمین دارید که در نخ اصلی نیستید.
RunLoop.main — یک شی CFRunLoop مرتبط با صف رویداد اصلی iOS است. این منابع ورودی (touch events)، تایمرها و بلوکهای DispatchQueue.main را پردازش میکند. هر فریم رندر (۶۰/۱۲۰ FPS) نیاز به تکمیل تمام عملیات در RunLoop قبل از پالس همگامسازی عمودی (VSync) دارد. اگر عملیات در Main Thread بیش از ۱۶.۶ میلیثانیه (۶۰ FPS) یا ۸.۳ میلیثانیه (۱۲۰ FPS) طول بکشد، برنامه فریمها را از دست میدهد که از نظر بصری به صورت jank یا stutter ظاهر میشود.
Looper.getMainLooper() — مکانیسم اصلی Android برای کار با نخ اصلی است. هر Main Thread در Android یک Looper دارد که به طور نامحدود پیامها را از صف (MessageQueue) استخراج کرده و برای پردازش به Handler ارسال میکند. Activity.runOnUiThread() و View.post() — اینها wrapperهای سطح بالای Handler(Looper.getMainLooper()) هستند. Kotlin Coroutines با Dispatchers.Main — روش مدرن بازگشت به نخ اصلی.
Android همچنین StrictMode را ارائه میدهد — ابزاری برای تشخیص عملیاتهایی که Main Thread را مسدود میکنند. StrictMode.setThreadPolicy() به شما امکان میدهد خط مشی تعیین کنید: ممنوعیت فراخوانیهای شبکه (NetworkPolicy)، خواندن از دیسک (DiskRead)، نوشتن روی دیسک (DiskWrite) در نخ اصلی. در صورت نقض خط مشی، استثنا ایجاد میشود یا پیامی در logcat نوشته میشود.
// Android: کار با Main Thread و Kotlin Coroutines
import android.os.Bundle
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URL
class MainActivity : ComponentActivity() {
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textView = TextView(this)
setContentView(textView)
// مثال: بارگذاری ناهمگام داده
lifecycleScope.launch {
val result = loadData() // اجرا در Dispatchers.IO
textView.text = result // UI در Main Thread
}
}
private suspend fun loadData(): String {
return withContext(Dispatchers.IO) {
URL("https://api.example.com/data").readText()
}
}
}
// StrictMode برای تشخیص نقضهای Main Thread
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
مثال در Kotlin استفاده صحیح از Dispatchers.Main را از طریق lifecycleScope.launch و Dispatchers.IO را از طریق withContext نشان میدهد. تمام کار شبکه روی IO-dispatcher اجرا میشود و بهروزرسانی TextView — به طور خودکار روی Main Thread، زیرا launch در lifecycleScope به طور پیشفرض از Dispatchers.Main استفاده میکند. StrictMode در Application.onCreate() فراخوانیهای تصادفی شبکه و عملیات دیسک را در نخ اصلی میگیرد.
Main Thread Checker — ابزار داخلی Xcode (موجود از Xcode 9) که فراخوانیهای UIKit، AppKit و سایر فریمورکهای UI را از نخهای پسزمینه پیدا میکند. در طول دیباگ، Main Thread Checker تمام فراخوانیهای UI-API را تحلیل میکند و در صورت تشخیص نقض، breakpoint با stack trace دقیق نشان میدهد. در دستگاههای واقعی (در نسخه release) Main Thread Checker کار نمیکند — نقضها به صورت crash یا رفتار نادرست ظاهر میشوند.
در Android معادل آن StrictMode است (در بالا توضیح داده شده) و آشکارساز لاگ داخلی: هنگام فراخوانی View.setText() یا View.invalidate() از نخ پسزمینه، Android CalledFromWrongThreadException را پرتاب میکند. علاوه بر این، Android Studio Profiler نشان میدهد که کدام عملیات در Main Thread اجرا میشوند. اگر عملیات شبکه یا فایل را در Main Thread میبینید — این نشانه قطعی مشکل است.
| ابزار | پلتفرم | چه چیزی را تشخیص میدهد |
|---|---|---|
| Main Thread Checker | iOS (Xcode) | فراخوانیهای UIKit/AppKit از نخهای پسزمینه |
| StrictMode | Android | شبکه، دیسک، عملیات طولانی در Main Thread |
| Android Studio Profiler | Android | تجسم بار Main Thread در طول زمان |
| Time Profiler | iOS (Instruments) | اندازهگیری زمان اجرای متدها در Main Thread |
| HUD / DispatchQueue.main.async | iOS | نشانه بصری مسدود شدن UI از طریق دیباگ |
قابلتوجهترین علامت مسدود شدن Main Thread — janky scroll (اسکرول ناپیوسته). هنگامی که کاربر UITableView یا RecyclerView را اسکرول میکند، سیستم انتظار دارد که فریم بعدی در ۱۶ میلیثانیه آماده شود. اگر در Main Thread رمزگشایی تصویر یا تجزیه JSON اجرا شود، رندر فریم به تأخیر میافتد و کاربر تکانها را میبیند. برای تشخیص از پروفایلر استفاده کنید: اگر متد prepareDisplay() یا layoutSubviews() >۱۶ میلیثانیه طول بکشد — دادهها در نخ اشتباه پردازش میشوند.
سناریوی اول — درخواست شبکه همگام از طریق URLConnection یا Data(contentsOf:) در Main Thread. در Android StrictMode با detectNetwork() بلافاصله این نقض را میگیرد. در iOS URLSession همگام خطای واضحی نمیدهد، اما UI به مدت درخواست (۱-۱۰ ثانیه) قفل میشود. راهحل: از URLSession.dataTask (iOS) یا Retrofit/OkHttp (Android) با callback ناهمگام استفاده کنید.
سناریوی دوم — رمزگشایی و فشردهسازی تصاویر. UIImage(data:) یا BitmapFactory.decodeResource() در Android در نخ اصلی — یکی از شایعترین علل jank. تصویر ۴۰۰۰×۳۰۰۰ پیکسل ۵۰-۱۵۰ میلیثانیه رمزگشایی میشود که از حد ۱۶ میلیثانیه فراتر میرود. راهحل: از ImageLoader (Kingfisher, Coil, Glide) استفاده کنید که رمزگشایی را در نخ پسزمینه تضمین میکنند.
سناریوی سوم — تجزیه JSON. تجزیه پاسخ API از طریق JSONSerialization (iOS) یا JSONObject (Android) در Main Thread. حتی یک JSON کوچک ۱۰۰ کیلوبایتی ۵-۱۵ میلیثانیه تجزیه میشود، اما در دستگاههای کند — تا ۵۰ میلیثانیه. در ترکیب با عملیات دیگر این جمع میشود و به فریمهای از دست رفته منجر میشود. راهحل: از kotlinx.serialization/Decodable با فراخوانی parse() در نخ پسزمینه استفاده کنید و فقط تخصیص نتیجه را در Main Thread بگذارید.
سوالات متداول
Main Thread — نخ اصلی برنامه که تمام عملیات UI روی آن اجرا میشود: پردازش لمس، رندر صفحه، انیمیشنها، بهروزرسانی layout. در iOS این RunLoop.main و DispatchQueue.main است، در Android — Looper.getMainLooper(). تمام فریمورکهای UI (UIKit, Android Views) thread-unsafe هستند و نیاز به فراخوانی فقط از Main Thread دارند. هر عملیات طولانی روی این نخ رابط را مسدود میکند.
فریمورکهای UI از نظر معماری برای عملکرد thread-unsafe هستند: همگامسازی دسترسی از طریق قفلها ۲۰-۴۰٪ سربار به هر عملیات رندر اضافه میکرد. توسعهدهندگان UIKit و Android مدل یک نخ را انتخاب کردند که در آن race condition بدون mutex حذف میشود. تمام تغییرات UI باید دقیقاً در Main Thread انجام شوند — در غیر این صورت crash یا نمایش نادرست.
در iOS از DispatchQueue.main.async { } برای ارسال کد به صف اصلی استفاده کنید. در Android — runOnUiThread { } یا Kotlin Coroutines با Dispatchers.Main. رویکرد مدرن — کوروتینها: withContext(Dispatchers.IO) برای کار پسزمینه و Dispatchers.Main خودکار در launch. برای پروژههای Java Handler(Looper.getMainLooper()).post { }.
ANR (Application Not Responding) — دیالوگ Android که اگر Main Thread بیش از ۵ ثانیه مسدود شود ظاهر میشود. ANR به این معنی است که سیستم پاسخی از برنامه به رویداد ورودی (لمس، فشار کلید) دریافت نکرده یا BroadcastReceiver در ۱۰ ثانیه تمام نشده است. علت — عملیات همگام در Main Thread: درخواست شبکه، کار با پایگاه داده، محاسبات پیچیده. در iOS معادل — قفل شدن UI بدون دیالوگ.
SwiftUI به طور خودکار تضمین میکند که body و modifier در Main Thread اجرا میشوند. با این حال، تغییر ویژگیهای @Published یا State از نخ پسزمینه (مثلاً از delegate URLSession) ممکن است مشکلاتی ایجاد کند. از @MainActor برای کلاسهای ObservableObject استفاده کنید تا تمام متدهای آنها در Main Thread اجرا شوند. در SwiftUI 5.5+ @MainActor به طور خودکار برای ObservableObject اضافه میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید