Android性能優(yōu)化死鎖監(jiān)控知識點詳解
前言
“死鎖”,這個從接觸程序開發(fā)的時候就會經(jīng)常聽到的詞,它其實也可以被稱為一種“藝術(shù)”,即互斥資源訪問循環(huán)的藝術(shù),在Android中,如果主線程產(chǎn)生死鎖,那么通常會以ANR結(jié)束app的生命周期,如果是兩個子線程的死鎖,那么就會白白浪費cpu的調(diào)度資源,同時也不那么容易被發(fā)現(xiàn),就像一顆“腫瘤”,永遠藏在app中。當然,本篇介紹的是業(yè)內(nèi)常見的死鎖監(jiān)控手段,同時也希望通過死鎖,去挖掘更加底層的知識,同時讓我們更加了解一些常用的監(jiān)控手段。
我們很容易模擬一個死鎖操作,比如
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都同時持有著對方想要的臨界資源(互斥資源),因此這兩個線程都處在互相等待對方的狀態(tài)。
死鎖檢測
我們怎么判斷死鎖:是否存在一個線程所持有的鎖被另一個線程所持有,同時另一個線程也持有該線程所需要的鎖,因此我們需要知道以下信息才能進行死鎖分析:
- 線程所要獲取的鎖是什么
- 該鎖被什么線程所持有
- 是否產(chǎn)生循環(huán)依賴的限制(本篇就不涉及了,因為我們知道了前兩個就可以自行分析了)
線程Block狀態(tài)
通過我們對synchronized的了解,當線程多次獲取不到鎖的時候,此時線程就會進入悲觀鎖狀態(tài),因此線程就會嘗試進入阻塞狀態(tài),避免進一步的cpu資源消耗,因此此時兩個線程都會處于block 阻塞的狀態(tài),我們就能知道,處于被block狀態(tài)的線程就有可能產(chǎn)生死鎖(只是有可能),我們可以通過遍歷所有線程,查看是否處于block狀態(tài),來進行死鎖判斷的第一步
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
}
通過對線程的判斷,我們能夠排除大部分非死鎖的線程,那么下一步我們要怎么做呢?如果線程發(fā)生了死鎖,那么一定擁有一個已經(jīng)持有的互斥資源并且不釋放才有可能造成死鎖對不對!那么我們下一步,就是要檢測當前線程所持有的鎖,如果兩個線程同時持有對方所需要的鎖,那么就會產(chǎn)生死鎖
獲取當前線程所請求的鎖
雖然我們在java層沒有相關(guān)的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 提供了一個方法查詢當前線程想要的鎖對象,這個鎖對象以O(shè)bjPtrmirror::Object對象表示,其中mirror::Object類型是art中相對應(yīng)于java層的Object類的代表,我們了解一下即可??吹竭@里我們可能還有一個疑問,這個Thread* thread的入?yún)⑹鞘裁茨??(其實是nativePeer,下文我們會了解)
我們有辦法能夠查詢到線程當前請求的鎖,那么這個鎖被誰持有呢?只有解決這兩個問題,我們才能進行死鎖的判斷對不對,我們繼續(xù)往下
通過鎖獲取當前持有的線程
我們還記得上文中返回的鎖對象是以O(shè)bjPtrmirror::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();
}
}
}
這里函數(shù)比較簡單,如果當前調(diào)用正常,那么執(zhí)行的就是LockWord::kFatLocked,返回的是native層的Thread的tid,最終是以uint32_t類型表示
注意這里GetLockOwnerThreadId中返回的Thread id千萬不要跟Java層的Thread對象的tid混淆,這里的tid才是真正的線程id標識
線程啟動
我們來看一下native層主線程的啟動,它隨著art虛擬機的啟動隨即啟動,我們都知道java層的線程其實在沒有跟操作系統(tǒng)的線程綁定的時候,它只能算是一塊內(nèi)存!只要經(jīng)過與native線程綁定后,這時的Thread才能真正具備線程調(diào)度的能力,下面我們以主線程啟動舉例子:
thread.cc
void Thread::FinishStartup() {
Runtime* runtime = Runtime::Current();
CHECK(runtime->IsStarted());
// Finish attaching the main thread.
ScopedObjectAccess soa(Thread::Current());
// 這里是關(guān)鍵,為什么主線程稱為“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函數(shù),創(chuàng)建的名稱是“main”,CreatePeer是native線程中非常重要的存在,所有線程創(chuàng)建都經(jīng)過它,這個函數(shù)有點長,筆者這里做了刪減
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();
}
// 設(shè)置了線程名字
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;
}
// 設(shè)置Thread的各種屬性
jint thread_priority = GetNativePriority();
jboolean thread_is_daemon = as_daemon;
// 創(chuàng)建了一個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;
}
// 看到這里,非常關(guān)鍵,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調(diào)用,把java中的Thread 的nativePeer 進行了賦值,而賦值的內(nèi)容,正是通過了這個調(diào)用SetLongField
env->SetLongField(peer.get(),
WellKnownClasses::java_lang_Thread_nativePeer,
reinterpret_cast64<jlong>(self));
這里我們簡單了解一下SetLongField,如果進行過jni開發(fā)的同學(xué)應(yīng)該能過明白,其實就是把peer.get()得到的對象(其實就是java層的Thread對象)的nativePeer屬性,賦值為了self(native層的Thread對象的指針),并強轉(zhuǎn)換為了jlong類型。我們接下來回到j(luò)ava層
Thread.java private volatile long nativePeer;
說了一大堆,那么這個nativePeer究竟是個什么?通過上面的代碼分析,我們能夠明白了,Thread.java中的nativePeer就是一個指針,它所指向的內(nèi)容正是native層中的Thread

nativePeer 與 native Thread tid 與java Thread tid
經(jīng)過了上面一段落,我們了解了nativePeer,那么我們繼續(xù)對比一下java層Thread tid 與native層Thread tid。我們通過在kotlin/java中,調(diào)用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_結(jié)構(gòu)體中,并且其位于Thread對象的開頭,從內(nèi)存分布上看,tid位于state_and_flags、suspend_count、think_lock_thread_id之后,還記得我們上面說過的nativePeer嘛?我們一直強調(diào)native是Thread的指針對象

因此我們可以通過指針的偏移,從而算出nativePeer到tid的換算公式,即nativePeer指針向下偏移三位就找到了tid(因為state_and_flags,state_and_flags,think_lock_thread_id都是int類型,那么對應(yīng)的指針也就是int * )這里有點繞,因為涉及指針的內(nèi)容
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
實現(xiàn)就是
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_signal_MainActivity_nativePeer2Threadid(JNIEnv *env, jobject thiz,
jlong native_peer) {
if (native_peer != 0) {
//long 強轉(zhuǎn) int
int *pInt = reinterpret_cast<int *>(native_peer);
//地址 +3,得到 native id
pInt = pInt + 3;
return *pInt;
}
}
}
dlsym與調(diào)用
我們上面終于把死鎖能涉及到的點都講完,比如如何獲取線程所請求的鎖,當前鎖又被那個線程持有,如何通過nativePeer獲取Thread id 做了分析,但是還有一個點我們還沒能解決,就是如何調(diào)用這些函數(shù)。我們需要調(diào)用的是GetContendedMonitor,GetLockOwnerThreadId,這個時候dlsym系統(tǒng)調(diào)用就出來了,我們可以通過dlsym 進行調(diào)用我們想要調(diào)用的函數(shù)
void* dlsym(void* __handle, const char* __symbol);
這里的symbol是什么呢?其實我們所有的elf(so也是一種elf文件)的所有調(diào)用函數(shù)都會生成一個符號,代表著這個函數(shù),它在elf的.text中。而我們android中,就會通過加載so的方式加載系統(tǒng)庫,加載的系統(tǒng)庫libart.so里面就包含著我們想要調(diào)用的函數(shù)GetContendedMonitor,GetLockOwnerThreadId的符號
我們可以通過objdump -t libart.so 查看符號

這里我們直接給出來各個符號,讀者可以直接用objdump查看符號
GetContendedMonitor 對應(yīng)的符號是
_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE
GetLockOwnerThreadId 對應(yīng)的符號
sdk <= 29 _ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE >29是這個 _ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE
系統(tǒng)限制
然后到這里,我們還是沒能完成調(diào)用,因為dlsym等dl系列的系統(tǒng)調(diào)用,因為從Android 7.0開始,Android系統(tǒng)開始阻止App中直接使用dlopen(), dlsym()等函數(shù)打開系統(tǒng)動態(tài)庫,好家伙!谷歌大兄弟為了安全的考慮,做了很多限制。但是這個防君子不防程序員,業(yè)內(nèi)依舊有很多繞過系統(tǒng)的限制的方法,我們看一下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一個內(nèi)建函數(shù)(通常由編譯器添加),__builtin_return_address(0)用于返回當前函數(shù)的返回地址。
在__loader_dlsym 會進行返回地址的校驗,如果此時返回地址不是屬于系統(tǒng)庫的地址,那么調(diào)用就不成功,這也是art虛擬機保護手段,因此我們很容易就得出一個想法,我們是不是可以用系統(tǒng)的某個函數(shù)去調(diào)用dlsym,然后把結(jié)果給到我們自己的函數(shù)消費就可以了?是的,業(yè)內(nèi)已經(jīng)有很多這個方案了,比如ndk_dlopen
我們拿arm架構(gòu)進行分析,arm架構(gòu)中LR寄存器就是保存了當前函數(shù)的返回地址,那么我們是不是在調(diào)用dlsym時可以通過匯編代碼直接修改LR寄存器的地址為某個系統(tǒng)庫的函數(shù)地址就可以了?嗯!是的,但是我們還需要把原來的LR地址給保存起來,不然就沒辦法還原原來的調(diào)用了。

這里我們拿ndk_dlopen的實現(xiàn)舉例子
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架構(gòu)匯編實現(xiàn)
#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作為了混淆系統(tǒng)調(diào)用的stub,我們參照著流程圖去理解上述代碼:
- 02 E0 A0 E1 mov lr, r2 把r2寄存器的內(nèi)容放到了lr寄存器,這個r2存的東西就是FatalError的地址
- 0x0000000000000008: 13 FF 2F E1 bx r3 ,通過bx指令調(diào)轉(zhuǎn),就可以正常執(zhí)行我們的dlsym了,r3就是我們自己的dlsym的地址
- 0x0000000000000000: 0C 00 BD E8 pop {r2, r3} 調(diào)用完r3寄存器的方法把r2寄存器放到調(diào)用棧下,提供給后面的執(zhí)行進行消費
- 0x0000000000000004: 12 FF 2F E1 bx r2 ,最后就回到了我們的r2,完成了一次調(diào)用
總之,我們想要做到dl系列的調(diào)用,就是想盡方法去修改對應(yīng)架構(gòu)的函數(shù)返回地址的數(shù)值。
死鎖檢測所有代碼
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、打開動態(tài)庫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、調(diào)用一下獲取monitor的函數(shù),返回當前線程想要競爭的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 強轉(zhuǎn) 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;
}
對應(yīng)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
總結(jié)
我們通過死鎖這個例子,去了解了native層Thread的相關(guān)方法,同時也了解了如何使用dlsym打開函數(shù)符號并調(diào)用。本篇Android性能優(yōu)化就到此結(jié)束,更多關(guān)于Android性能優(yōu)化死鎖監(jiān)控的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android關(guān)于按鈕點擊效果實現(xiàn)的方法
今天小編就為大家分享一篇關(guān)于android關(guān)于按鈕點擊效果實現(xiàn)的方法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03
Android EditText限制輸入整數(shù)和小數(shù)的位數(shù)的方法示例
這篇文章主要介紹了Android EditText限制輸入整數(shù)和小數(shù)的位數(shù)的方法示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
Android車載空調(diào)系統(tǒng)(HVAC)開發(fā)方法分析
HVAC?全稱:供暖通風與空氣調(diào)節(jié)(Heating?Ventilation?and?Air?Conditioning),用戶可以通過他來控制整個汽車的空調(diào)系統(tǒng),是汽車中非常重要的一個功能,汽車的空調(diào)HMI雖然并不復(fù)雜,但是大多都是用符號來表示功能,必須理解空調(diào)的各個符號表示的含義2023-12-12

