FastJNI导致的Android系统死机问题分析
1. 問題現象
- 問題發生的Android系統版本是7.0(Nougat);
-
滑動屏幕和按鍵都無響應,屏幕內容沒有任何刷新;
-
watchdog沒有重啟system_server;
-
問題現場可以連接adb;
2. 初步分析
對于死機問題,我們需要做一些分析前的準備工作:
(1)拿到問題現場,及時充電以保證問題現場不被破壞;
(2)如果沒有現場可以忽略這一步,通過kill -3 后面跟上system_server pid命令產生一份最新的traces文件;
(3)如果最新的traces文件無法產生,則通過debuggerd -b $system_server pid打印出一份所有線程的Native調用棧到文件中;
(4)通過adb將/data/anr下的文件都pull出來;
(5)通過adb將/data/tombstones下的文件都pull出來;
問題現場通過kill -3命令沒有產生最新時間點的traces文件,因此只能查看/data/anr下最新時間點的traces文件,但是發現traces文件中的時間點已經是昨天的:
----- pid 1487 at 2017-04-25 22:44:52 ----- Cmd line: system_server并且昨天生成的這份traces文件中system_server的各個線程的狀態都正常,沒有明顯的問題和block。
接著分析由debuggerd -b $system_server pid打印出的Native調用棧信息,首先查看watchdog線程當前所處的狀態,為什么沒有重啟手機:
"watchdog" sysTid=1877#00 pc 000000000001bf6c /system/lib64/libc.so (syscall+28)#01 pc 00000000000e7ac8 /system/lib64/libart.so (_ZN3art17ConditionVariable16WaitHoldingLocksEPNS_6ThreadE+160)#02 pc 000000000037ac68 /system/lib64/libart.so (_ZN3art7Monitor4WaitEPNS_6ThreadElibNS_11ThreadStateE+896)#03 pc 000000000054e980 /system/framework/arm64/boot.oat (offset 0x54e000) (java.lang.Object.wait+140)#04 pc 000000000054e8b8 /system/framework/arm64/boot.oat (offset 0x54e000) (java.lang.Object.wait+52)#05 pc 00000000011035a8 /system/framework/oat/arm64/services.odex (offset 0xf0c000)發現watchdog等待在ConditionVariable的WaitHoldingLocks方法上,為什么會等在這里?等在這里是否正常?
帶著問題我們通過調用棧中的地址和addr2line工具層層定位具體的代碼,首先是從Object的wait方法調用Monitor的Wait方法,具體代碼如下:
/* art/runtime/monitor.cc */ 579void Monitor::Wait(Thread* self, int64_t ms, int32_t ns, 580 bool interruptShouldThrow, ThreadState why) {... 631 632 bool was_interrupted = false; 633 { 634 // Update thread state. If the GC wakes up, it'll ignore us, knowing 635 // that we won't touch any references in this state, and we'll check 636 // our suspend mode before we transition out. 637 ScopedThreadSuspension sts(self, why);... 651 652 // Handle the case where the thread was interrupted before we called wait(). 653 if (self->IsInterruptedLocked()) { 654 was_interrupted = true; 655 } else { 656 // Wait for a notification or a timeout to occur. 657 if (why == kWaiting) { 658 self->GetWaitConditionVariable()->Wait(self); 659 } else { 660 DCHECK(why == kTimedWaiting || why == kSleeping) << why; 661 self->GetWaitConditionVariable()->TimedWait(self, ms, ns); 662 } 663 was_interrupted = self->IsInterruptedLocked(); 664 } 665 }接著在Monitor的Wait方法中,調用self->GetWaitConditionVariable()->Wait或者TimedWait方法之前會通過ScopedThreadSuspension類的構造方法進行線程狀態的切換,從Runable狀態切換到Suspended狀態,切換的具體代碼如下:
/* art/runtime/scoped_thread_state_change.h */ 280// Annotalysis helper for going to a suspended state from runnable. 281class ScopedThreadSuspension : public ValueObject { 282 public: 283 explicit ScopedThreadSuspension(Thread* self, ThreadState suspended_state) ...{ 288 DCHECK(self_ != nullptr); 289 self_->TransitionFromRunnableToSuspended(suspended_state); 290 }隨后self->GetWaitConditionVariable()->Wait或者TimedWait方法執行完,即等待條件滿足或者超時后會繼續往下執行,執行出了ScopedThreadSuspension對象sts所在代碼塊的作用域之后會執行ScopedThreadSuspension類的析構方法,在析構方法中會再次進行線程狀態切換,從Suspended狀態切換到Runable狀態,切換的具體代碼如下:
/* art/runtime/thread-inl.h */ 172inline ThreadState Thread::TransitionFromSuspendedToRunnable() {... 177 do {... 195 } else if ((old_state_and_flags.as_struct.flags & kActiveSuspendBarrier) != 0) { 196 PassActiveSuspendBarriers(this); 197 } else if ((old_state_and_flags.as_struct.flags & kCheckpointRequest) != 0) { 198 // Impossible 199 LOG(FATAL) << "Transitioning to runnable with checkpoint flag, " 200 << " flags=" << old_state_and_flags.as_struct.flags 201 << " state=" << old_state_and_flags.as_struct.state; 202 } else if ((old_state_and_flags.as_struct.flags & kSuspendRequest) != 0) { 203 // Wait while our suspend count is non-zero.... 207 while ((old_state_and_flags.as_struct.flags & kSuspendRequest) != 0) { 208 // Re-check when Thread::resume_cond_ is notified. 209 Thread::resume_cond_->Wait(this); 210 old_state_and_flags.as_int = tls32_.state_and_flags.as_int; 211 DCHECK_EQ(old_state_and_flags.as_struct.state, old_state); 212 } 213 DCHECK_EQ(GetSuspendCount(), 0); 214 } 215 } while (true);在從Suspended狀態切換到Runable狀態切換的過程會判斷是否有人發起了suspend請求,當前watchdog調用棧就是因為有人發起了kSuspendRequest而執行到Thread::resume_cond_->Wait方法,在Thread::resume_cond_->Wait方法中調用了WaitHoldingLocks方法,具體代碼如下:
/* art/runtime/base/mutex.cc */ 834void ConditionVariable::Wait(Thread* self) { 835 guard_.CheckSafeToWait(self); 836 WaitHoldingLocks(self); 837} 838 839void ConditionVariable::WaitHoldingLocks(Thread* self) {... 850 if (futex(sequence_.Address(), FUTEX_WAIT, cur_sequence, nullptr, nullptr, 0) != 0) { 851 // Futex failed, check it is an expected error. 852 // EAGAIN == EWOULDBLK, so we let the caller try again. 853 // EINTR implies a signal was sent to this thread. 854 if ((errno != EINTR) && (errno != EAGAIN)) { 855 PLOG(FATAL) << "futex wait failed for " << name_; 856 } 857 }在WaitHoldingLocks方法中調用了futex函數并最終等待在futex函數中的系統調用上,具體代碼如下:
/* art/runtime/base/mutex-inl.h */ 43static inline int futex(volatile int *uaddr, int op, int val, const struct timespec *timeout, 44 volatile int *uaddr2, int val3) { 45 return syscall(SYS_futex, uaddr, op, val, timeout, uaddr2, val3); 46}通過上面的分析我們知道watchdog線程等待在futex系統調用上的原因是有人發起了kSuspendRequest,使其在從suspended狀態切換到Runable狀態的時候進入等待,那什么情況會發起kSuspendRequest呢?
比較常見和正常的情況是GC線程在第二次標記清除的時候以及Signal Catcher在Dump trace的時候會SuspendAll線程,在suspend的過程中會給每個線程發起kSuspendRequest,接下來我們先看看GC線程是否在做SuspendAll的操作,具體調用棧如下:
"HeapTaskDaemon" sysTid=1497#00 pc 000000000001bf6c /system/lib64/libc.so (syscall+28)#01 pc 000000000046035c /system/lib64/libart.so (_ZN3art10ThreadList18SuspendAllInternalEPNS_6ThreadES2_S2_b+628)#02 pc 00000000004609c8 /system/lib64/libart.so (_ZN3art10ThreadList10SuspendAllEPKcb+536)#03 pc 00000000001ea0e0 /system/lib64/libart.so (_ZN3art2gc9collector9MarkSweep9RunPhasesEv+232)...從GC線程的調用棧中可以看到,它確實是在做SuspendAll的操作,到這里就解釋了為什么watchdog會等待在Thread::resume_cond_->WaitHoldingLocks。
正常情況下SuspendAll操作在很短的時間內就會完成,然后ResumeAll恢復所有等待在Thread::resume_cond_->WaitHoldingLocks的線程以繼續執行,但是通過debuggerd -b $system_server pid來多次打印Native調用??梢源_定GC線程一直沒有完成SuspendAll操作,導致包括watchdog線程內的很多其他線程在從suspended狀態切換到Runable狀態的時候都等待在Thread::resume_cond_->WaitHoldingLocks上,但是為什么GC線程一直完成不了SuspendAll操作呢?
3. 深入分析
帶著初步分析的線索和問題,我們繼續分析,GC線程完成SuspendAll的前提是除了GC線程自己之外所有其他線程都切換到非Runable狀態,以此來保護Java空間的數據和狀態,所以如果有線程一直無法切換到非Runable狀態,則GC線程就會一直無法完成SuspendAll操作,順著這條線索我們繼續分析,看system_server進程中那個線程當前還處在Runable狀態,當前沒有完整的包含Java調用棧的traces文件只有Native的調用棧,所以無法直接判斷那個線程還處在Runable狀態,這個時候怎么辦?
我們換一個思路,采用排除法,如果一個線程等待在Thread::resume_cond_->WaitHoldingLocks上,那它一定響應了GC線程發起的kSuspendRequest切換為了非Runable狀態,根據這個條件先進行初步過濾,將等待在Thread::resume_cond_->WaitHoldingLocks的線程排除掉,剩下的線程在邏輯上大致可以分為兩種狀態,一種是在非Runable狀態執行Native方法或者block在標準的系統調用和libc函數上,這種狀態的線程不會影響GC線程的SuspendAll操作,另外一種是在Runable狀態執行FastJNI方法,這種狀態的線程如果不能及時的執行完發生block就會直接block GC線程的SuspendAll操作,并且這種FastJNI方法的調用一般都伴隨著業務邏輯代碼的上下文。
JNI是Java Native Interface的縮寫,Java代碼和Native代碼進行相互操作的API接口稱為Java本地接口。
根據上面的思路和線索,多次打印調用棧,發現一個可疑線程android.display一直block在同一個位置,并且上下文是在執行業務相關的代碼,具體調用棧如下:
"android.display" sysTid=1509#00 pc 000000000001bf6c /system/lib64/libc.so (syscall+28)#01 pc 0000000000068cb8 /system/lib64/libc.so (_ZL33__pthread_mutex_lock_with_timeoutP24pthread_mutex_internal_tbPK8timespec+248)#02 pc 00000000000fc9f8 /system/lib64/libandroid_runtime.so#03 pc 0000000001cf2078 /system/framework/arm64/boot-framework.oat (offset 0x1965000) (android.content.res.AssetManager.applyStyle+244)#04 pc 0000000001d19bbc /system/framework/arm64/boot-framework.oat (offset 0x1965000) (android.content.res.ResourcesImpl$ThemeImpl.obtainStyledAttributes+280)#05 pc 0000000001d18628 /system/framework/arm64/boot-framework.oat (offset 0x1965000) (android.content.res.Resources$Theme.obtainStyledAttributes+100)#06 pc 0000000002654068 /system/framework/arm64/boot-framework.oat (offset 0x1965000) (android.view.animation.DecelerateInterpolator.<init>+132)...找到初步的嫌疑線程之后,我們再進一步確認其block的方法是否FastJNI方法,通過代碼搜索可以看到android.content.res.AssetManager.applyStyle的定義確實是FastJNI,具體代碼如下:
/* frameworks/base/core/jni/android_util_AssetManager.cpp */ 2228 { "applyStyle","!(JIIJ[I[I[I)Z", 2229 (void*) android_content_AssetManager_applyStyle },到這里基本確認block GC線程SuspendAll操作的線程至少有android.display了,順著這個線索我們繼續分析為什么android.display線程一直block在這個FastJNI方法上。
FastJNI即快速Java本地接口,和普通JNI的區別在于快,因為快所以從Java代碼調用到FastJNI代碼的時候不會將線程的狀態從Runable切換到Native,而是一直保持Runable執行,其定義方式是在參數簽名的簽名加上!。
通過調用棧和addr2line定位到block的代碼在如下的1432行:
/* frameworks/base/core/jni/android_util_AssetManager.cpp */ 1342static jboolean android_content_AssetManager_applyStyle(JNIEnv* env, jobject clazz,... 1350{... 1369 1370 ResTable::Theme* theme = reinterpret_cast<ResTable::Theme*>(themeToken); 1371 const ResTable& res = theme->getResTable();... 1430 1431 // Now lock down the resource object and start pulling stuff from it. 1432 res.lock();最終因為拿不到AssetManager的ResTable中的mLock而block,具體代碼如下:
/* frameworks/base/libs/androidfw/ResourceTypes.cpp */ 4193void ResTable::lock() const 4194{ 4195 mLock.lock(); 4196}既然android.display線程拿不到這個mLock就說明已經有其他線程拿到了,接下來繼續在system_server的調用棧中搜尋執行AssetManager以及ResTable相關代碼的線程調用棧,發現一個可疑線程Binder:1487_17的調用棧如下:
"Binder:1487_17" sysTid=4827#00 pc 000000000001bf6c /system/lib64/libc.so (syscall+28)#01 pc 00000000000e7ac8 /system/lib64/libart.so (_ZN3art17ConditionVariable16WaitHoldingLocksEPNS_6ThreadE+160)#02 pc 000000000034aeb8 /system/lib64/libart.so (_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc+300)#03 pc 00000000000f9508 /system/lib64/libandroid_runtime.so#04 pc 0000000001cf2b38 /system/framework/arm64/boot-framework.oat (offset 0x1965000) (android.content.res.AssetManager.getArrayStringResource+132)#05 pc 0000000001cf5914 /system/framework/arm64/boot-framework.oat (offset 0x1965000) (android.content.res.AssetManager.getResourceStringArray+48)...從調用棧來看Binder:1487_17已經等待在Thread::resume_cond_->WaitHoldingLocks上,成功切換到非Runable狀態了,但是上面的調用棧中執行了AssetManager相關的操作,很可能持有了android.display線程需要的mLock,為了準確定位到是誰block了android.display線程,我們繼續addr2line看一下Binder:1487_17的調用棧,發現確實持有了mLock,具體代碼如下:
/* frameworks/base/core/jni/android_util_AssetManager.cpp */ 1939static jobjectArray android_content_AssetManager_getArrayStringResource(JNIEnv* env, jobject clazz, jint arrayResId) 1941{... 1949 const ssize_t N = res.lockBag(arrayResId, &startOfBag);... 1963 for (size_t i=0; ((ssize_t)i)<N; i++, bag++) {... 1975 if (value.dataType == Res_value::TYPE_STRING) {... 1978 if (str8 != NULL) { 1979 str = env->NewStringUTF(str8); 1980 } else { 1981 ... 1984 } 1985... 2003}lockBag方法中獲取了mLock,具體定義如下:
/* frameworks/base/libs/androidfw/ResourceTypes.cpp */ 4176ssize_t ResTable::lockBag(uint32_t resID, const bag_entry** outBag) const 4177{ 4178 mLock.lock();... 4185}Binder:1487_17線程在持有了ResTable的mLock之后,接著執行NewStringUTF操作的過程中需要將線程狀態切換到Runable,在切換的時候發現GC線程發起了kSuspendRequest,接著Binder:1487_17線程就等待在了Thread::resume_cond_->WaitHoldingLocks上,至此死鎖環已經形成,但是還有一個疑問就是GC線程的SuspendAll操作從代碼上初步來看是有等待超時的,但是為什么超時機制沒有生效?
SuspendAll操作超時的邏輯代碼如下:
/* art/runtime/thread_list.cc */ 503void ThreadList::SuspendAll(const char* cause, bool long_suspend) {... 515 SuspendAllInternal(self, self);... 520 if (Locks::mutator_lock_->ExclusiveLockWithTimeout(self, kThreadSuspendTimeoutMs, 0)) { 521 break; 522 } else if (!long_suspend_) {... 526 UnsafeLogFatalForThreadSuspendAllTimeout(); 527 } 528 }先執行SuspendAllInternal隨后獨占持有muator lock并指定超時的時間為kThreadSuspendTimeoutMs,即在30s內要獨占獲取到mutator lock,kThreadSuspendTimeoutMs定義如下:
/* art/runtime/thread_list.cc */ static constexpr uint64_t kThreadSuspendTimeoutMs = 30 * 1000; // 30s.mutator lock,即突變鎖,顧名思義是為了防止虛擬機中的狀態包括Java對象、堆內存等突然變化而設置的鎖,常見的使用場景和用途有線程狀態切換、GC以及Dump trace等,當線程從非Runable狀態切換Runnable狀態的時候會shared held mutator lock,當GC第二次標記清理的時候會SuspendAll線程使其進入非Runnable狀態并獨占mutator lock,當dump trace的時候signal catcher線程在AOSP原生流程也會SuspendAll線程使其進入非Runnable狀態并獨占mutator lock,主要原因是dump heap狀態快照需要讓所有線程停下來防止它們再改變虛擬機中的堆內存狀態。
30s超時獲取不到mutator lock則執行UnsafeLogFatalForThreadSuspendAllTimeout方法,在方法中執行exit退出進程,方法具體定義如下:
/* art/runtime/thread_list.cc */ 294NO_RETURN static void UnsafeLogFatalForThreadSuspendAllTimeout() {... 301 LOG(FATAL) << ss.str(); 302 exit(0); 303}正常情況下SuspendAllInternal操作執行完之后所有線程都已經越過Suspend柵欄并釋放mutator lock,同時線程處于非Runable狀態,但是通過addr2line定位GC線程的調用棧所在的源代碼,發現GC線程并沒有執行完SuspendAllInternal操作,所以就沒有執行到獨占mutator lock的超時操作,而是block在了SuspendAllInternal方法中的futex wait,關鍵代碼如下:
/* art/runtime/thread_list.cc */ 515void ThreadList::SuspendAllInternal(Thread* self, Thread* ignore1, Thread* ignore2, bool debug_suspend) {... 588 InitTimeSpec(true, CLOCK_MONOTONIC, 10000, 0, &wait_timeout); 589 while (true) {... 592#if ART_USE_FUTEXES 593 if (futex(pending_threads.Address(), FUTEX_WAIT, cur_val, &wait_timeout, nullptr, 0) != 0) { 594 // EAGAIN and EINTR both indicate a spurious failure, try again from the beginning. 595 if ((errno != EAGAIN) && (errno != EINTR)) { 596 if (errno == ETIMEDOUT) { 597 LOG(kIsDebugBuild ? FATAL : ERROR) << "Unexpected time out during suspend all."; 598 } else { 599 PLOG(FATAL) << "futex wait failed for SuspendAllInternal()"; 600 }... 615}初步從代碼來看futex的wait也是有超時的,但是為什么仍然會陷入block?
經過多次打印調用棧及查看log,發現GC線程一直還在運行,并每隔10s左右就打出time out的log,順著這個線索審查代碼,發現等待的代碼存在缺陷,當kIsDebugBuild條件不滿足,suspend all time out的時候只會打出一句error log,并在while中不斷循環,沒有退出的條件,通過對比Android M和Android N的代碼發現這個新的wait機制是新加的,沒有考慮全面,在Android M的時候不存在當前的問題,會直接走到mutator lock的獨占持有并設置超時,Android M suspend all的超時機制關鍵代碼如下:
/* art/runtime/thread_list.cc */ 454void ThreadList::SuspendAll(const char* cause, bool long_suspend) {... 486 // Block on the mutator lock until all Runnable threads release their share of access. 487#if HAVE_TIMED_RWLOCK 488 while (true) { 489 if (Locks::mutator_lock_->ExclusiveLockWithTimeout(self, kThreadSuspendTimeoutMs, 0)) { 490 break; 491 } else if (!long_suspend_) {... 495 UnsafeLogFatalForThreadSuspendAllTimeout(); 496 } 497 }到這里SuspendAll超時機制為什么沒有生效的問題就得到了解釋。
4. 問題總結
總結一下問題的死鎖流程:
- GC線程被調度到,在第二次標記清除的時候執行了SuspendAll的操作,給每個線程都發起了kSuspendRequest,同時將每個線程的sCount都做了加1操作,最后在SuspendAllInternal方法中等待所有線程都釋放mutator lock并進入非Runable狀態;
- 接著在Native執行代碼的Binder:1487_17線程被調度到,在執行的過程中持有了ResTable的mLock,隨后在執行NewStringUTF時需要將線程狀態切換到Runable,在切換的時候發現GC線程發起了kSuspendRequest,接著Binder:1487_17線程就等待在了Thread::resume_cond_->WaitHoldingLocks上;
- 在FastJNI方法執行過程中的android.display線程被調度到,由于android.content.res.AssetManager.applyStyle是FastJNI方法,所以其線程狀態是Runable,但在執行android.content.res.AssetManager.applyStyle的過程中需要獲取ResTable的mLock,由于ResTable的mLock已經被Binder:1487_17線程持有,所以導致android.display線程在獲取mLock的時候block并一直處于Runable狀態;
- GC線程由于SuspendAll的超時機制存在缺陷,在有線程一直處于Runable狀態時會進入死循環永遠等待;
- GC線程被android.display線程block,android.display線程被Binder:1487_17線程block,Binder:1487_17線程被GC線程block,誰也無法剝奪誰持有的資源,并且滿足不了繼續運行的條件,最終導致死鎖;
5. 解決方案
通過初步分析、深入分析和問題總結,我們清楚的知道了問題的原因,接下來我們再分析一下如何解決這個問題:
- SuspendAll的超時機制存在缺陷,需要首先解決,通過查看最新的AOSP代碼發現Google已經修復,合入Google的修復之后如果再出現上面的問題場景就不會再凍屏,而是觸發SuspendAll的超時機制重啟;
- FastJNI應該是執行非??觳荒苡袟l件依賴的JNI,android.content.res.AssetManager.applyStyle方法很明顯不符合這個條件,所以通過將android.content.res.AssetManager.applyStyle方法的FastJNI描述符移除將其變成普通的JNI,android.display線程就會正常進入非Runable狀態,從而不影響GC線程的SuspendAll操作,死鎖環即可正常斷開;
修復SuspendAll超時機制缺陷的patch去掉了kIsDebugBuild條件判斷,在所有版本中只要等待超時就打印FATAL log,并在FATAL LOG對象析構時執行abort,退出進行,修復的關鍵代碼如下:
/* art/runtime/thread_list.cc */ 560void ThreadList::SuspendAllInternal(Thread* self, 561 Thread* ignore1, 562 Thread* ignore2, 563 bool debug_suspend) {... 636 InitTimeSpec(false, CLOCK_MONOTONIC, kIsDebugBuild ? 50000 : 60000, 0, &wait_timeout);... 639 while (true) {... 643 if (futex(pending_threads.Address(), FUTEX_WAIT, cur_val, &wait_timeout, nullptr, 0) != 0) { 644 // EAGAIN and EINTR both indicate a spurious failure, try again from the beginning. 645 if ((errno != EAGAIN) && (errno != EINTR)) { 646 if (errno == ETIMEDOUT) { 647 LOG(FATAL) 648 << "Timed out waiting for threads to suspend, waited for " 649 << PrettyDuration(NanoTime() - start_time); 650 }LOG(FATAL)是一個宏定義,最終會被替換為:?:LogMessage(FILE, LINE, severity, -1).stream(),LogMessage的析構函數中會根據severity是否是FATAL來決定是否abort,宏定義代碼如下:
/* art/runtime/base/logging.h */ 92// Logs a message to logcat on Android otherwise to stderr. If the severity is FATAL it also causes 93// an abort. For example: LOG(FATAL) << "We didn't expect to reach here"; 94#define LOG(severity) ::art::LogMessage(__FILE__, __LINE__, severity, -1).stream()LogMessage析構中abort的關鍵代碼如下:
195LogMessage::~LogMessage() {... 229 // Abort if necessary. 230 if (data_->GetSeverity() == FATAL) { 231 Runtime::Abort(msg.c_str()); 232 } 233}將android.content.res.AssetManager.applyStyle方法JNI定義的簽名前的!描述符去掉,調整其為非FastJNI的patch如下:
/* frameworks/base/core/jni/android_util_AssetManager.cpp */ 2228 { "applyStyle","(JIIJ[I[I[I)Z", 2229 (void*) android_content_AssetManager_applyStyle },6. 實戰心得
-
對于JNI方法不能隨便將其調整為FastJNI,調整為FastJNI后執行此方法是將不做線程的狀態切換,會導致線程一直處于Runable狀態,直到FastJNI方法執行完畢;
-
FastJNI方法必須要滿足執行快沒有依賴的條件,否則不恰當的FastJNI聲明和優化可能會帶來不可預料的死鎖或者線程狀態問題;
總結
以上是生活随笔為你收集整理的FastJNI导致的Android系统死机问题分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 几款游戏引擎简介
- 下一篇: Android 仿qq 点赞功能