
前言“死锁”这个从接触程序开发的时候就会经常听到的词它其实也可以被称为一种“艺术”即互斥资源访问循环的艺术在Android中如果主线程产生死锁那么通常会以ANR结束app的生命周期如果是两个子线程的死锁那么就会白白浪费cpu的调度资源同时也不那么容易被发现就像一颗“肿瘤”永远藏在app中。当然本篇介绍的是业内常见的死锁监控手段同时也希望通过死锁去挖掘更加底层的知识同时让我们更加了解一些常用的监控手段。我们很容易模拟一个死锁操作比如val lock1 Object() val lock2 Object() Thread ({ synchronized(lock1){ Thread.sleep(2000) synchronized(lock2){ } } },thread222).start() Thread ({ synchronized(lock2) { Thread.sleep(1000) synchronized(lock1) { } } },thread111).start()因为thread111跟thread222都同时持有着对方想要的临界资源互斥资源因此这两个线程都处在互相等待对方的状态。死锁检测我们怎么判断死锁是否存在一个线程所持有的锁被另一个线程所持有同时另一个线程也持有该线程所需要的锁因此我们需要知道以下信息才能进行死锁分析线程所要获取的锁是什么该锁被什么线程所持有是否产生循环依赖的限制本篇就不涉及了因为我们知道了前两个就可以自行分析了线程Block状态通过我们对synchronized的了解当线程多次获取不到锁的时候此时线程就会进入悲观锁状态因此线程就会尝试进入阻塞状态避免进一步的cpu资源消耗因此此时两个线程都会处于block 阻塞的状态我们就能知道处于被block状态的线程就有可能产生死锁只是有可能我们可以通过遍历所有线程查看是否处于block状态来进行死锁判断的第一步val threads getAllThread() threads.forEach { if(it?.isAlive true it.state Thread.State.BLOCKED){ 进入死锁判断 } }获取所有线程private fun getAllThread():ArrayThread?{ val threadGroup Thread.currentThread().threadGroup; val total Thread.activeCount() val array arrayOfNullsThread(total) threadGroup?.enumerate(array) return array }通过对线程的判断我们能够排除大部分非死锁的线程那么下一步我们要怎么做呢如果线程发生了死锁那么一定拥有一个已经持有的互斥资源并且不释放才有可能造成死锁对不对那么我们下一步就是要检测当前线程所持有的锁如果两个线程同时持有对方所需要的锁那么就会产生死锁获取当前线程所请求的锁虽然我们在java层没有相关的api提供给我们获取线程当前想要请求的锁但是在我们的native层却可以轻松做到因为它在art中得到更多的支持。ObjPtrmirror::Object Monitor::GetContendedMonitor(Thread* thread) { // This is used to implement JDWPs ThreadReference.CurrentContendedMonitor, and has a bizarre // definition of contended that includes a monitor a thread is trying to enter... ObjPtrmirror::Object result thread-GetMonitorEnterObject(); if (result nullptr) { // ...but also a monitor that the thread is waiting on. MutexLock mu(Thread::Current(), *thread-GetWaitMutex()); Monitor* monitor thread-GetWaitMonitor(); if (monitor ! nullptr) { result monitor-GetObject(); } } return result; }其中第一步尝试着通过thread-GetMonitorEnterObject()去拿mirror::Object* GetMonitorEnterObject() const REQUIRES_SHARED(Locks::mutator_lock_) { return tlsPtr_.monitor_enter_object; }其中tlsPtr_其实就是art虚拟机中对于线程ThreadLocal的代表即代表着只属于线程的本地对象会先尝试从这里拿拿不到的话通过Thread类中的wait_mutex_对象去拿Mutex* GetWaitMutex() const LOCK_RETURNED(wait_mutex_) { return wait_mutex_; }GetContendedMonitor 提供了一个方法查询当前线程想要的锁对象这个锁对象以ObjPtrmirror::Object对象表示其中mirror::Object类型是art中相对应于java层的Object类的代表我们了解一下即可。看到这里我们可能还有一个疑问这个Thread* thread的入参是什么呢其实是nativePeer下文我们会了解我们有办法能够查询到线程当前请求的锁那么这个锁被谁持有呢只有解决这两个问题我们才能进行死锁的判断对不对我们继续往下通过锁获取当前持有的线程我们还记得上文中返回的锁对象是以ObjPtrmirror::Object表示的当然art中同样提供了方法让我们通过这个锁对象去查询当前是哪个线程持有uint32_t Monitor::GetLockOwnerThreadId(ObjPtrmirror::Object obj) { DCHECK(obj ! nullptr); LockWord lock_word obj-GetLockWord(true); switch (lock_word.GetState()) { case LockWord::kHashCode: // Fall-through. case LockWord::kUnlocked: return ThreadList::kInvalidThreadId; case LockWord::kThinLocked: return lock_word.ThinLockOwner(); case LockWord::kFatLocked: { Monitor* mon lock_word.FatLockMonitor(); return mon-GetOwnerThreadId(); } default: { LOG(FATAL) Unreachable; UNREACHABLE(); } } }这里函数比较简单如果当前调用正常那么执行的就是LockWord::kFatLocked返回的是native层的Thread的tid最终是以uint32_t类型表示注意这里GetLockOwnerThreadId中返回的Thread id千万不要跟Java层的Thread对象的tid混淆这里的tid才是真正的线程id标识线程启动我们来看一下native层主线程的启动它随着art虚拟机的启动随即启动我们都知道java层的线程其实在没有跟操作系统的线程绑定的时候它只能算是一块内存只要经过与native线程绑定后这时的Thread才能真正具备线程调度的能力下面我们以主线程启动举例子thread.cc void Thread::FinishStartup() { Runtime* runtime Runtime::Current(); CHECK(runtime-IsStarted()); // Finish attaching the main thread. ScopedObjectAccess soa(Thread::Current()); // 这里是关键为什么主线程称为“main线程”的原因 soa.Self()-CreatePeer(main, false, runtime-GetMainThreadGroup()); soa.Self()-AssertNoPendingException(); runtime-RunRootClinits(soa.Self()); soa.Self()-NotifyThreadGroup(soa, runtime-GetMainThreadGroup()); soa.Self()-AssertNoPendingException(); }可以看到为什么主线程被称为“主线程”是因为在art虚拟机启动的时候通过CreatePeer函数创建的名称是“main”CreatePeer是native线程中非常重要的存在所有线程创建都经过它这个函数有点长,笔者这里做了删减void Thread::CreatePeer(const char* name, bool as_daemon, jobject thread_group) { Runtime* runtime Runtime::Current(); CHECK(runtime-IsStarted()); JNIEnv* env tlsPtr_.jni_env; if (thread_group nullptr) { thread_group runtime-GetMainThreadGroup(); } // 设置了线程名字 ScopedLocalRefjobject thread_name(env, env-NewStringUTF(name)); // Add missing null check in case of OOM b/18297817 if (name ! nullptr thread_name.get() nullptr) { CHECK(IsExceptionPending()); return; } // 设置Thread的各种属性 jint thread_priority GetNativePriority(); jboolean thread_is_daemon as_daemon; // 创建了一个java层的Thread对象名字叫做peer ScopedLocalRefjobject peer(env, env-AllocObject(WellKnownClasses::java_lang_Thread)); if (peer.get() nullptr) { CHECK(IsExceptionPending()); return; } { ScopedObjectAccess soa(this); tlsPtr_.opeer soa.Decodemirror::Object(peer.get()).Ptr(); } env-CallNonvirtualVoidMethod(peer.get(), WellKnownClasses::java_lang_Thread, WellKnownClasses::java_lang_Thread_init, thread_group, thread_name.get(), thread_priority, thread_is_daemon); if (IsExceptionPending()) { return; } // 看到这里非常关键self 指向了当前native Thread对象 self-Thread Thread* self this; DCHECK_EQ(self, Thread::Current()); env-SetLongField(peer.get(), WellKnownClasses::java_lang_Thread_nativePeer, reinterpret_cast64jlong(self)); ScopedObjectAccess soa(self); StackHandleScope1 hs(self); .... }这里其实就是一次jni调用把java中的Thread 的nativePeer 进行了赋值而赋值的内容正是通过了这个调用SetLongFieldenv-SetLongField(peer.get(), WellKnownClasses::java_lang_Thread_nativePeer, reinterpret_cast64jlong(self));这里我们简单了解一下SetLongField如果进行过jni开发的同学应该能过明白其实就是把peer.get()得到的对象其实就是java层的Thread对象的nativePeer属性赋值为了selfnative层的Thread对象的指针并强转换为了jlong类型。我们接下来回到java层Thread.java private volatile long nativePeer;说了一大堆那么这个nativePeer究竟是个什么通过上面的代码分析我们能够明白了Thread.java中的nativePeer就是一个指针它所指向的内容正是native层中的ThreadnativePeer 与 native Thread tid 与java Thread tid经过了上面一段落我们了解了nativePeer那么我们继续对比一下java层Thread tid 与native层Thread tid。我们通过在kotlin/java中调用Thread对象的id属性其实得到的是这个private long tid;它的生成方法如下/* Set thread ID */ tid nextThreadID();private static synchronized long nextThreadID() { return threadSeqNumber; }可以看到虽然它的确能代表一个java层中Thread的标识但是生成其实可以看到他也仅仅是一个普通的累积id生成同时也并没有在native层中被当作唯一标识进行使用。而native Thread 的 tid属性才是真正的线程id在art中通过GetTid获取pid_t GetTid() const { return tls32_.tid; }同时我们也可以注意到tid 是保存在 tls32_结构体中并且其位于Thread对象的开头从内存分布上看tid位于state_and_flags、suspend_count、think_lock_thread_id之后还记得我们上面说过的nativePeer嘛我们一直强调native是Thread的指针对象因此我们可以通过指针的偏移从而算出nativePeer到tid的换算公式即nativePeer指针向下偏移三位就找到了tid因为state_and_flagsstate_and_flagsthink_lock_thread_id都是int类型那么对应的指针也就是int * 这里有点绕因为涉及指针的内容int *pInt reinterpret_castint *(native_peer); //地址 3得到tid pInt pInt 3; return *pInt;nativePeer对象因为就在java层我们很容易通过反射就能拿到val nativePeer Thread::class.java.getDeclaredField(nativePeer) nativePeer.isAccessible true val currentNativePeer nativePeer.get(it)这里我们通过nativePeer换算成tid可以写成一个jni方法external fun nativePeer2Threadid(nativePeer:Long):Int实现就是extern C JNIEXPORT jint JNICALL Java_com_example_signal_MainActivity_nativePeer2Threadid(JNIEnv *env, jobject thiz, jlong native_peer) { if (native_peer ! 0) { //long 强转 int int *pInt reinterpret_castint *(native_peer); //地址 3得到 native id pInt pInt 3; return *pInt; } } }dlsym与调用我们上面终于把死锁能涉及到的点都讲完比如如何获取线程所请求的锁当前锁又被那个线程持有如何通过nativePeer获取Thread id 做了分析但是还有一个点我们还没能解决就是如何调用这些函数。我们需要调用的是GetContendedMonitorGetLockOwnerThreadId这个时候dlsym系统调用就出来了我们可以通过dlsym 进行调用我们想要调用的函数void* dlsym(void* __handle, const char* __symbol);这里的symbol是什么呢其实我们所有的elfso也是一种elf文件的所有调用函数都会生成一个符号代表着这个函数它在elf的.text中。而我们android中就会通过加载so的方式加载系统库加载的系统库libart.so里面就包含着我们想要调用的函数GetContendedMonitorGetLockOwnerThreadId的符号我们可以通过objdump -t libart.so 查看符号这里我们直接给出来各个符号读者可以直接用objdump查看符号GetContendedMonitor 对应的符号是_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadEGetLockOwnerThreadId 对应的符号sdk 29 _ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE 29是这个 _ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE系统限制然后到这里我们还是没能完成调用因为dlsym等dl系列的系统调用因为从Android 7.0开始Android系统开始阻止App中直接使用dlopen(), dlsym()等函数打开系统动态库好家伙谷歌大兄弟为了安全的考虑做了很多限制。但是这个防君子不防程序员业内依旧有很多绕过系统的限制的方法我们看一下dlsym__attribute__((__weak__)) void* dlsym(void* handle, const char* symbol) { const void* caller_addr __builtin_return_address(0); return __loader_dlsym(handle, symbol, caller_addr); }__builtin_return_address是Linux一个内建函数通常由编译器添加__builtin_return_address(0)用于返回当前函数的返回地址。在__loader_dlsym 会进行返回地址的校验如果此时返回地址不是属于系统库的地址那么调用就不成功这也是art虚拟机保护手段因此我们很容易就得出一个想法我们是不是可以用系统的某个函数去调用dlsym然后把结果给到我们自己的函数消费就可以了是的业内已经有很多这个方案了比如ndk_dlopen我们拿arm架构进行分析arm架构中LR寄存器就是保存了当前函数的返回地址那么我们是不是在调用dlsym时可以通过汇编代码直接修改LR寄存器的地址为某个系统库的函数地址就可以了嗯是的但是我们还需要把原来的LR地址给保存起来不然就没办法还原原来的调用了。这里我们拿ndk_dlopen的实现举例子if (SDK_INT 0) { char sdk[PROP_VALUE_MAX]; __system_property_get(ro.build.version.sdk, sdk); SDK_INT atoi(sdk); LOGI(SDK_INT %d, SDK_INT); if (SDK_INT 24) { static __attribute__((__aligned__(PAGE_SIZE))) uint8_t __insns[PAGE_SIZE]; STUBS.generic_stub __insns; mprotect(__insns, sizeof(__insns), PROT_READ | PROT_WRITE | PROT_EXEC); // we are currently hijacking FatalError as a fake system-call trampoline uintptr_t pv (uintptr_t)(*env)-FatalError; uintptr_t pu (pv | (PAGE_SIZE - 1)) 1u; uintptr_t pd (pv ~(PAGE_SIZE - 1)); mprotect((void *)pd, pv 8u pu ? PAGE_SIZE * 2u : PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC); quick_on_stack_back (void *)pv; // arm架构汇编实现 #elif defined(__arm__) // r0~r3 /* 0x0000000000000000: 08 E0 2D E5 str lr, [sp, #-8]! 0x0000000000000004: 02 E0 A0 E1 mov lr, r2 0x0000000000000008: 13 FF 2F E1 bx r3 */ memcpy(__insns, \x08\xE0\x2D\xE5\x02\xE0\xA0\xE1\x13\xFF\x2F\xE1, 12); if ((pv 1u) ! 0u) { // Thumb /* 0x0000000000000000: 0C BC pop {r2, r3} 0x0000000000000002: 10 47 bx r2 */ memcpy((void *)(pv - 1), \x0C\xBC\x10\x47, 4); } else { /* 0x0000000000000000: 0C 00 BD E8 pop {r2, r3} 0x0000000000000004: 12 FF 2F E1 bx r2 */ memcpy(quick_on_stack_back, \x0C\x00\xBD\xE8\x12\xFF\x2F\xE1, 8); } //if其中我们拿(*env)-FatalError作为了混淆系统调用的stub我们参照着流程图去理解上述代码02 E0 A0 E1 mov lr, r2 把r2寄存器的内容放到了lr寄存器这个r2存的东西就是FatalError的地址0x0000000000000008: 13 FF 2F E1 bx r3 ,通过bx指令调转就可以正常执行我们的dlsym了r3就是我们自己的dlsym的地址0x0000000000000000: 0C 00 BD E8 pop {r2, r3} 调用完r3寄存器的方法把r2寄存器放到调用栈下提供给后面的执行进行消费0x0000000000000004: 12 FF 2F E1 bx r2 最后就回到了我们的r2完成了一次调用总之我们想要做到dl系列的调用就是想尽方法去修改对应架构的函数返回地址的数值。死锁检测所有代码const char *get_lock_owner_symbol_name() { if (SDK_INT 29) { return _ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE; } else { return _ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE; } } extern C JNIEXPORT jint JNICALL Java_com_example_signal_MyHandler_deadLockMonitor(JNIEnv *env, jobject thiz, jlong native_thread) { //1、初始化 ndk_init(env); //2、打开动态库libart.so void *so_addr ndk_dlopen(libart.so, RTLD_NOLOAD); void * get_contended_monitor ndk_dlsym(so_addr, _ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE); void * get_lock_owner_thread ndk_dlsym(so_addr, get_lock_owner_symbol_name()); int monitor_thread_id 0; if (get_contended_monitor ! nullptr get_lock_owner_thread ! nullptr) { //1、调用一下获取monitor的函数返回当前线程想要竞争的monitor int monitorObj ((int (*)(long)) get_contended_monitor)(native_thread); if (monitorObj ! 0) { // 2、获取这个monitor被哪个线程持有返回该线程id monitor_thread_id ((int (*)(int)) get_lock_owner_thread)(monitorObj); } else { monitor_thread_id 0; } } return monitor_thread_id; } extern C JNIEXPORT jint JNICALL Java_com_example_signal_MainActivity_nativePeer2Threadid(JNIEnv *env, jobject thiz, jlong native_peer) { if (native_peer ! 0) { if (SDK_INT 20) { //long 强转 int int *pInt reinterpret_castint *(native_peer); //地址 3得到 native id pInt pInt 3; return *pInt; } } } extern C jint JNI_OnLoad(JavaVM *vm, void *reserved) { char sdk[PROP_VALUE_MAX]; __system_property_get(ro.build.version.sdk, sdk); SDK_INT atoi(sdk); return JNI_VERSION_1_4; }对应java层external fun deadLockMonitor(nativeThread:Long):Int private fun getAllThread():ArrayThread?{ val threadGroup Thread.currentThread().threadGroup; val total Thread.activeCount() val array arrayOfNullsThread(total) threadGroup?.enumerate(array) return array } external fun nativePeer2Threadid(nativePeer:Long):Int总结我们通过死锁这个例子去了解了native层Thread的相关方法同时也了解了如何使用dlsym打开函数符号并调用。本篇Android性能优化就到此结束感谢观看作者Pika链接https://juejin.cn/post/7159784805293359141最后如果想要成为架构师或想突破20~30K薪资范畴那就不要局限在编码业务要会选型、扩展提升编程思维。此外良好的职业规划也很重要学习的习惯很重要但是最重要的还是要能持之以恒任何不能坚持落实的计划都是空谈。如果你没有方向这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》帮大家将杂乱、零散、碎片化的知识进行体系化的整理让大家系统而高效地掌握Android开发的各个知识点。相对于我们平时看的碎片化内容这份笔记的知识点更系统化更容易理解和记忆是严格按照知识体系编排的。一、架构师筑基必备技能1、深入理解Java泛型2、注解深入浅出3、并发编程4、数据传输与序列化5、Java虚拟机原理6、高效IO二、Android百大框架源码解析1.Retrofit 2.0源码解析2.Okhttp3源码解析3.ButterKnife源码解析4.MPAndroidChart 源码解析5.Glide源码解析6.Leakcanary 源码解析7.Universal-lmage-Loader源码解析8.EventBus 3.0源码解析9.zxing源码分析10.Picasso源码解析11.LottieAndroid使用详解及源码解析12.Fresco 源码分析——图片加载流程三、Android性能优化实战解析腾讯Bugly:对字符串匹配算法的一点理解爱奇艺安卓APP崩溃捕获方案——xCrash字节跳动深入理解Gradle框架之一Plugin, Extension, buildSrc百度APP技术Android H5首屏优化实践支付宝客户端架构解析Android 客户端启动速度优化之「垃圾回收」携程从智行 Android 项目看组件化架构实践网易新闻构建优化如何让你的构建速度“势如闪电”四、高级kotlin强化实战1、Kotlin入门教程2、Kotlin 实战避坑指南3、项目实战《Kotlin Jetpack 实战》从一个膜拜大神的 Demo 开始Kotlin 写 Gradle 脚本是一种什么体验Kotlin 编程的三重境界Kotlin 高阶函数Kotlin 泛型Kotlin 扩展Kotlin 委托协程“不为人知”的调试技巧图解协程suspend五、Android高级UI开源框架进阶解密1.SmartRefreshLayout的使用2.Android之PullToRefresh控件源码解析3.Android-PullToRefresh下拉刷新库基本用法4.LoadSir-高效易用的加载反馈页管理框架5.Android通用LoadingView加载框架详解6.MPAndroidChart实现LineChart折线图7.hellocharts-android使用指南8.SmartTable使用指南9.开源项目android-uitableview介绍10.ExcelPanel 使用指南11.Android开源项目SlidingMenu深切解析12.MaterialDrawer使用指南六、NDK模块开发1、NDK 模块开发2、JNI 模块3、Native 开发工具4、Linux 编程5、底层图片处理6、音视频开发7、机器学习七、Flutter技术进阶1、Flutter跨平台开发概述2、Windows中Flutter开发环境搭建3、编写你的第一个Flutter APP4、Flutter开发环境搭建和调试5、Dart语法篇之基础语法(一)6、Dart语法篇之集合的使用与源码解析(二)7、Dart语法篇之集合操作符函数与源码分析(三)八、微信小程序开发1、小程序概述及入门2、小程序UI开发3、API操作4、购物商场项目实战……全套视频资料一、面试合集二、源码解析合集三、开源框架合集欢迎大家一键三连支持若需要文中资料直接点击文末CSDN官方认证微信卡片免费领取↓↓↓