قفل شدن (ویسنت) — حالتی است که در آن برنامه موبایل برای مدت طولانی به هیچ اقدام کاربر واکنش نشان نمیدهد. بر خلاف لگها (کند شدن کار) و گلیچها (رفتار نادرست)، قفل شدن رابط کاربری را کاملاً مسدود میکند: لمسها پردازش نمیشوند، انیمیشن متوقف میشود، صفحه «یخ میزند». علت — مسدود شدن نخ اصلی توسط عملیات همزمان، بنبست در کد چندنخی یا جمعآوری زباله غیرعادی طولانی. طبق Apple Main Thread Checker Documentation, بیش از ۴۰٪ گزارشهای crash در iOS با مسدود شدن نخ اصلی مرتبط است. در Android وضعیت مشابه منجر به ANR — دیالوگ سیستمی «برنامه پاسخ نمیدهد» میشود.
نکات اصلی
قفل شدن (freeze, hang) در برنامه موبایل — حالتی است که در آن برنامه پردازش رویدادهای ورودی و بهروزرسانی رابط را برای چند ثانیه یا بیشتر متوقف میکند. از نظر فنی به این معنی است که نخ اصلی (main thread) مسدود شده و نمیتواند چرخه بعدی runner را اجرا کند.
لگ — تأخیر تا ۵۰۰ میلیثانیه است که در آن کاربر کندی را متوجه میشود، اما برنامه به کار ادامه میدهد. قفل شدن از ۱ ثانیه تا دهها ثانیه طول میکشد. ANR در Android — حالت خاصی از قفل شدن است که بیش از ۵ ثانیه طول کشیده و توسط سیستم شناسایی شده است. هر قفل شدنی منجر به ANR نمیشود، اما هر ANR یک قفل شدن مستند شده توسط سیستم است.
در Android قفل شدن بیش از ۵ ثانیه باعث دیالوگ ANR با پیشنهاد بستن برنامه میشود. در iOS سیستم دارای watchdog است — اگر برنامه به رویدادها در عرض ۱۰–۲۰ ثانیه واکنش نشان ندهد، Watchdog فرآیند را با کد 0x8badf00d (ate bad food) خاتمه میدهد. کاربر فقط بسته شدن ناگهانی برنامه و بازگشت به صفحه اصلی را میبیند.
هر عملیاتی که بیش از ۱۰۰ میلیثانیه طول بکشد و در نخ اصلی اجرا شود، به طور بالقوه باعث قفل شدن میشود. منابع اصلی مسدودیت را بررسی میکنیم.
خواندن فایل بزرگ، درخواست شبکه بدون ناهمزمانی، ذخیره دادهها در SharedPreferences با روش همزمان apply به دنبال commit — همه این عملیات نخ اصلی را مسدود میکنند. در Android خواندن همزمان یک فایل ۱۰ مگابایتی بسته به سرعت حافظه فلش میتواند ۲۰۰–۵۰۰ میلیثانیه طول بکشد. در iOS بارگذاری همزمان URLSession بدون completionHandler رابط کاربری را برای مدت پاسخ سرور مسدود میکند.
وقتی دو نخ منتظر آزاد شدن منابعی هستند که توسط یکدیگر نگه داشته شدهاند، بنبست (deadlock) رخ میدهد. در برنامههای موبایل سناریوی معمول — نخ A Lock1 را قفل کرده و منتظر Lock2 است، و نخ B Lock2 را قفل کرده و منتظر Lock1 است. هر دو نخ برای همیشه قفل میشوند. اگر یکی از آنها نخ اصلی باشد، برنامه کاملاً قفل میشود.
خطا در منطق — مثلاً while(true) بدون شرط خروج یا بازگشت بدون حالت پایه — منجر به اجرای بینهایت در نخ اصلی میشود. Android این را پس از ۵ ثانیه از طریق ANR تشخیص میدهد، iOS — از طریق Stackshot که stack فراخوانی بینهایت تکراری را ثبت میکند.
تشخیص قفل شدن نیازمند ابزارهایی است که بتوانند وضعیت همه نخها را در لحظه مسدودیت ثبت کنند.
در هر ANR سیستم Android فایل /data/anr/traces.txt را ذخیره میکند که شامل dump stack هر نخ برنامه است. تحلیل این فایل — روش اصلی تشخیص: باید نخ main را پیدا کرد و دید روی چه متدی متوقف شده است. اگر stack به Thread.sleep, InputStream.read یا Lock.lock ختم شود — علت پیدا شده است.
Xcode هنگام قفل شدن برنامه (سیگنال SIGSTOP) میتواند Stackshot — تصویری از stackهای همه نخها را بگیرد. در طرح «Logging» → «Include Stackshot Logs» را فعال کنید. هنگام crash با کد 0x8badf00d لاگ crash را از Devices & Simulators استخراج کرده و نخ com.apple.main-thread را با stack قفل شده پیدا کنید.
Main Thread Checker به طور خودکار فراخوانیهای UIKit از نخهای پسزمینه را در حین کار برنامه تشخیص میدهد. آن را در طرح فعال کنید (Diagnostics → Main Thread Checker). هر هشدار یک علت بالقوه قفل شدن است، به خصوص اگر در بسته شدن completionHandler درخواست شبکه رخ دهد.
مثال تشخیص مسدودیت از طریق StrictMode در Android:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
رفع قفل شدن با انتقال تمام عملیاتهای بالقوه طولانی به نخهای پسزمینه شروع میشود. تکنیکهای خاص هر پلتفرم را بررسی میکنیم.
Kotlin Coroutines با viewModelScope.launch(Dispatchers.IO) تضمین میکنند که عملیات شبکه یا خواندن از پایگاه داده در نخ پسزمینه اجرا میشوند. Dispatchers.Main فقط برای بهروزرسانی رابط کاربری استفاده میشود. مهم: همه توابع suspend باید ساختاریافته باشند — کوروتینهای فرزند با لغو والد لغو میشوند و از نشت نخ جلوگیری میکنند.
Grand Central Dispatch با DispatchQueue.global(qos: .userInitiated) برای وظایف پسزمینه و DispatchQueue.main.async برای بهروزرسانی رابط کاربری — الگوی استاندارد. از sync() در صف اصلی اجتناب کنید — این بنبست تضمینی است. از async/await (Swift 5.5+) برای کد ناهمزمان خواناتر با بازگشت خودکار به نخ اصلی از طریق MainActor استفاده کنید.
بلوکهای synchronized در Kotlin و @synchronized در Swift در نخ اصلی خطرناک هستند: اگر نخ دیگری قبلاً این قفل را گرفته باشد، نخ اصلی در انتظار قفل میشود. از انواع اتمیک (AtomicInteger, ویژگیهای اتمیک در Swift) یا صفهای متوالی به جای قفلها استفاده کنید.
مثال بارگذاری ناهمزمان داده با کوروتینها در Android:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
جلوگیری از قفل شدن به صورت سیستماتیک با ترکیب ابزارها، اصول معماری و فرآیندهای بازبینی کد کمک میکند.
StrictMode را با penaltyDeath برای سیاستهای نخی پیکربندی کنید — این باعث crash فوری برنامه هنگام تشخیص فراخوانی شبکه یا ورودی-خروجی دیسک در نخ اصلی میشود. توسعهدهنده نمیتواند مشکل را نادیده بگیرد. در build تولیدی از penaltyLog برای جمعآوری آمار بدون crash استفاده کنید.
در iOS Main Thread Checker را در طرح Debug فعال کنید و CI را برای اجرای تستها با این گزینه پیکربندی کنید. اگر تست حاوی فراخوانی UIKit از نخ پسزمینه باشد — باید ناموفق شود. این تنها راه قابل اعتماد برای شناسایی مشکل قبل از ارسال به TestFlight است.
به فرآیند بازبینی کد یک بند اجباری اضافه کنید: بررسی اینکه هر فراخوانی شبکه، کار با فایلها، پایگاه داده یا محاسبات سنگین در نخ پسزمینه اجرا میشود. بنبست را میتوان با تحلیلگر ایستا تشخیص داد: Infer از Facebook و Thread Safety Checker از Xcode بنبستهای بالقوه را قبل از اجرا پیدا میکنند.
سؤالات متداول
ANR (Application Not Responding) — اعلان سیستمی Android است که هنگام قفل شدن نخ اصلی بیش از ۵ ثانیه ظاهر میشود. قفل شدن مفهوم گستردهتری است: هر مسدودیت رابط کاربری با هر مدتی. در iOS ANR وجود ندارد، اما Watchdog با مهلت ۱۰–۲۰ ثانیه وجود دارد.
فایل در /data/anr/traces.txt قرار دارد. برای دسترسی نیاز به root یا adb shell است: adb shell cat /data/anr/traces.txt \> traces.txt را با حقوق root اجرا کنید. در stack نخ «main» را پیدا کنید — آخرین متد فراخوانی شده علت مسدودیت را نشان میدهد.
اگر قفل شدن کمتر از ۱۰ ثانیه طول بکشد، Watchdog فعال نمیشود و برنامه تا پایان عملیات مسدودکننده فقط «قفل میماند». کاربر crash نمیبیند اما ناامیدی را تجربه میکند. برای تشخیص چنین مواردی از MetricKit با ردگیریهای سفارشی زمان اجرا استفاده کنید.
از تستهای رابط کاربری با بررسی باز شدن صفحه در کمتر از ۱ ثانیه استفاده کنید. به CI اندازهگیری زمان بین لمس و ظاهر شدن صفحه بعدی را اضافه کنید. در Android از Espresso با IdlingResource برای انتظار عملیات ناهمزمان استفاده کنید. در iOS XCTest با XCTWaiter برای بررسی زمان بارگذاری.
SwiftUI به خودی خود باعث قفل شدن نمیشود، اما محاسبات پیچیده در ویژگی body — بله. اگر body به دلیل عملیات سنگین به مدت ۵۰۰ میلیثانیه محاسبه شود، رابط کاربری قفل میشود. راه حل — محاسبات را به Task.detached منتقل کرده و @State را به صورت ناهمزمان در بازیگر اصلی بهروزرسانی کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.