Andoird性能优化 - 死锁监控与其背后的小知识
Posted 上马定江山
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Andoird性能优化 - 死锁监控与其背后的小知识相关的知识,希望对你有一定的参考价值。
前言
“死锁”,这个从接触程序开发的时候就会经常听到的词,它其实也可以被称为一种“艺术”,即互斥资源访问循环的艺术,在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():Array<Thread?>
val threadGroup = Thread.currentThread().threadGroup;
val total = Thread.activeCount()
val array = arrayOfNulls<Thread>(total)
threadGroup?.enumerate(array)
return array
通过对线程的判断,我们能够排除大部分非死锁的线程,那么下一步我们要怎么做呢?如果线程发生了死锁,那么一定拥有一个已经持有的互斥资源并且不释放才有可能造成死锁对不对!那么我们下一步,就是要检测当前线程所持有的锁,如果两个线程同时持有对方所需要的锁,那么就会产生死锁
获取当前线程所请求的锁
虽然我们在java层没有相关的api提供给我们获取线程当前想要请求的锁,但是在我们的native层,却可以轻松做到,因为它在art中得到更多的支持。
ObjPtr<mirror::Object> Monitor::GetContendedMonitor(Thread* thread)
// This is used to implement JDWP's ThreadReference.CurrentContendedMonitor, and has a bizarre
// definition of contended that includes a monitor a thread is trying to enter...
ObjPtr<mirror::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(ObjPtr<mirror::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();
// 设置了线程名字
ScopedLocalRef<jobject> 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
ScopedLocalRef<jobject> peer(env, env->AllocObject(WellKnownClasses::java_lang_Thread));
if (peer.get() == nullptr)
CHECK(IsExceptionPending());
return;
ScopedObjectAccess soa(this);
tlsPtr_.opeer = soa.Decode<mirror::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_cast64<jlong>(self));
ScopedObjectAccess soa(self);
StackHandleScope<1> hs(self);
....
这里其实就是一次jni调用,把java中的Thread 的nativePeer 进行了赋值,而赋值的内容,正是通过了这个调用SetLongField
env->SetLongField(peer.get(),
WellKnownClasses::java_lang_Thread_nativePeer,
reinterpret_cast64<jlong>(self));
这里我们简单了解一下SetLongField,如果进行过jni开发的同学应该能过明白,其实就是把peer.get()得到的对象(其实就是java层的Thread对象)的nativePeer属性,赋值为了self(native层的Thread对象的指针),并强转换为了jlong类型。我们接下来回到java层
Thread.java
private volatile long nativePeer;
说了一大堆,那么这个nativePeer究竟是个什么?通过上面的代码分析,我们能够明白了,Thread.java中的nativePeer就是一个指针,它所指向的内容正是native层中的Thread
nativePeer 与 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_flags,state_and_flags,think_lock_thread_id都是int类型,那么对应的指针也就是int * )这里有点绕,因为涉及指针的内容
int *pInt = reinterpret_cast<int *>(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_cast<int *>(native_peer);
//地址 +3,得到 native id
pInt = pInt + 3;
return *pInt;
dlsym与调用
我们上面终于把死锁能涉及到的点都讲完,比如如何获取线程所请求的锁,当前锁又被那个线程持有,如何通过nativePeer获取Thread id 做了分析,但是还有一个点我们还没能解决,就是如何调用这些函数。我们需要调用的是GetContendedMonitor,GetLockOwnerThreadId,这个时候dlsym系统调用就出来了,我们可以通过dlsym 进行调用我们想要调用的函数
void* dlsym(void* __handle, const char* __symbol);
这里的symbol是什么呢?其实我们所有的elf(so也是一种elf文件)的所有调用函数都会生成一个符号,代表着这个函数,它在elf的.text中。而我们android中,就会通过加载so的方式加载系统库,加载的系统库libart.so里面就包含着我们想要调用的函数GetContendedMonitor,GetLockOwnerThreadId的符号
我们可以通过objdump -t libart.so 查看符号
这里我们直接给出来各个符号,读者可以直接用objdump查看符号
GetContendedMonitor 对应的符号是
_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE
GetLockOwnerThreadId 对应的符号
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_cast<int *>(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():Array<Thread?>
val threadGroup = Thread.currentThread().threadGroup;
val total = Thread.activeCount()
val array = arrayOfNulls<Thread>(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 APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)
八、微信小程序开发
1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
以上是关于Andoird性能优化 - 死锁监控与其背后的小知识的主要内容,如果未能解决你的问题,请参考以下文章