Android性能優(yōu)化死鎖監(jiān)控知識(shí)點(diǎn)詳解
前言
“死鎖”,這個(gè)從接觸程序開(kāi)發(fā)的時(shí)候就會(huì)經(jīng)常聽(tīng)到的詞,它其實(shí)也可以被稱(chēng)為一種“藝術(shù)”,即互斥資源訪問(wèn)循環(huán)的藝術(shù),在Android中,如果主線程產(chǎn)生死鎖,那么通常會(huì)以ANR結(jié)束app的生命周期,如果是兩個(gè)子線程的死鎖,那么就會(huì)白白浪費(fèi)cpu的調(diào)度資源,同時(shí)也不那么容易被發(fā)現(xiàn),就像一顆“腫瘤”,永遠(yuǎn)藏在app中。當(dāng)然,本篇介紹的是業(yè)內(nèi)常見(jiàn)的死鎖監(jiān)控手段,同時(shí)也希望通過(guò)死鎖,去挖掘更加底層的知識(shí),同時(shí)讓我們更加了解一些常用的監(jiān)控手段。
我們很容易模擬一個(gè)死鎖操作,比如
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()
因?yàn)閠hread111跟thread222都同時(shí)持有著對(duì)方想要的臨界資源(互斥資源),因此這兩個(gè)線程都處在互相等待對(duì)方的狀態(tài)。
死鎖檢測(cè)
我們?cè)趺磁袛嗨梨i:是否存在一個(gè)線程所持有的鎖被另一個(gè)線程所持有,同時(shí)另一個(gè)線程也持有該線程所需要的鎖,因此我們需要知道以下信息才能進(jìn)行死鎖分析:
- 線程所要獲取的鎖是什么
- 該鎖被什么線程所持有
- 是否產(chǎn)生循環(huán)依賴(lài)的限制(本篇就不涉及了,因?yàn)槲覀冎懒饲皟蓚€(gè)就可以自行分析了)
線程Block狀態(tài)
通過(guò)我們對(duì)synchronized的了解,當(dāng)線程多次獲取不到鎖的時(shí)候,此時(shí)線程就會(huì)進(jìn)入悲觀鎖狀態(tài),因此線程就會(huì)嘗試進(jìn)入阻塞狀態(tài),避免進(jìn)一步的cpu資源消耗,因此此時(shí)兩個(gè)線程都會(huì)處于block 阻塞的狀態(tài),我們就能知道,處于被block狀態(tài)的線程就有可能產(chǎn)生死鎖(只是有可能),我們可以通過(guò)遍歷所有線程,查看是否處于block狀態(tài),來(lái)進(jìn)行死鎖判斷的第一步
val threads = getAllThread() threads.forEach { if(it?.isAlive == true && it.state == Thread.State.BLOCKED){ 進(jìn)入死鎖判斷 } }
獲取所有線程
private fun getAllThread():Array<Thread?>{ val threadGroup = Thread.currentThread().threadGroup; val total = Thread.activeCount() val array = arrayOfNulls<Thread>(total) threadGroup?.enumerate(array) return array }
通過(guò)對(duì)線程的判斷,我們能夠排除大部分非死鎖的線程,那么下一步我們要怎么做呢?如果線程發(fā)生了死鎖,那么一定擁有一個(gè)已經(jīng)持有的互斥資源并且不釋放才有可能造成死鎖對(duì)不對(duì)!那么我們下一步,就是要檢測(cè)當(dāng)前線程所持有的鎖,如果兩個(gè)線程同時(shí)持有對(duì)方所需要的鎖,那么就會(huì)產(chǎn)生死鎖
獲取當(dāng)前線程所請(qǐng)求的鎖
雖然我們?cè)趈ava層沒(méi)有相關(guān)的api提供給我們獲取線程當(dāng)前想要請(qǐng)求的鎖,但是在我們的native層,卻可以輕松做到,因?yàn)樗赼rt中得到更多的支持。
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; }
其中第一步嘗試著通過(guò)thread->GetMonitorEnterObject()去拿
mirror::Object* GetMonitorEnterObject() const REQUIRES_SHARED(Locks::mutator_lock_) { return tlsPtr_.monitor_enter_object; }
其中tlsPtr_ 其實(shí)就是art虛擬機(jī)中對(duì)于線程ThreadLocal的代表,即代表著只屬于線程的本地對(duì)象,會(huì)先嘗試從這里拿,拿不到的話(huà)通過(guò)Thread類(lèi)中的wait_mutex_對(duì)象去拿
Mutex* GetWaitMutex() const LOCK_RETURNED(wait_mutex_) { return wait_mutex_; }
GetContendedMonitor 提供了一個(gè)方法查詢(xún)當(dāng)前線程想要的鎖對(duì)象,這個(gè)鎖對(duì)象以O(shè)bjPtrmirror::Object對(duì)象表示,其中mirror::Object類(lèi)型是art中相對(duì)應(yīng)于java層的Object類(lèi)的代表,我們了解一下即可??吹竭@里我們可能還有一個(gè)疑問(wèn),這個(gè)Thread* thread的入?yún)⑹鞘裁茨??(其?shí)是nativePeer,下文我們會(huì)了解)
我們有辦法能夠查詢(xún)到線程當(dāng)前請(qǐng)求的鎖,那么這個(gè)鎖被誰(shuí)持有呢?只有解決這兩個(gè)問(wèn)題,我們才能進(jìn)行死鎖的判斷對(duì)不對(duì),我們繼續(xù)往下
通過(guò)鎖獲取當(dāng)前持有的線程
我們還記得上文中返回的鎖對(duì)象是以O(shè)bjPtrmirror::Object表示的,當(dāng)然,art中同樣提供了方法,讓我們通過(guò)這個(gè)鎖對(duì)象去查詢(xún)當(dāng)前是哪個(gè)線程持有
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ù)比較簡(jiǎn)單,如果當(dāng)前調(diào)用正常,那么執(zhí)行的就是LockWord::kFatLocked,返回的是native層的Thread的tid,最終是以u(píng)int32_t類(lèi)型表示
注意這里GetLockOwnerThreadId中返回的Thread id千萬(wàn)不要跟Java層的Thread對(duì)象的tid混淆,這里的tid才是真正的線程id標(biāo)識(shí)
線程啟動(dòng)
我們來(lái)看一下native層主線程的啟動(dòng),它隨著art虛擬機(jī)的啟動(dòng)隨即啟動(dòng),我們都知道java層的線程其實(shí)在沒(méi)有跟操作系統(tǒng)的線程綁定的時(shí)候,它只能算是一塊內(nèi)存!只要經(jīng)過(guò)與native線程綁定后,這時(shí)的Thread才能真正具備線程調(diào)度的能力,下面我們以主線程啟動(dòng)舉例子:
thread.cc void Thread::FinishStartup() { Runtime* runtime = Runtime::Current(); CHECK(runtime->IsStarted()); // Finish attaching the main thread. ScopedObjectAccess soa(Thread::Current()); // 這里是關(guān)鍵,為什么主線程稱(chēng)為“main線程”的原因 soa.Self()->CreatePeer("main", false, runtime->GetMainThreadGroup()); soa.Self()->AssertNoPendingException(); runtime->RunRootClinits(soa.Self()); soa.Self()->NotifyThreadGroup(soa, runtime->GetMainThreadGroup()); soa.Self()->AssertNoPendingException(); }
可以看到,為什么主線程被稱(chēng)為“主線程”,是因?yàn)樵赼rt虛擬機(jī)啟動(dòng)的時(shí)候,通過(guò)CreatePeer函數(shù),創(chuàng)建的名稱(chēng)是“main”,CreatePeer是native線程中非常重要的存在,所有線程創(chuàng)建都經(jīng)過(guò)它,這個(gè)函數(shù)有點(diǎn)長(zhǎng),筆者這里做了刪減
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)建了一個(gè)java層的Thread對(duì)象,名字叫做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 指向了當(dāng)前native Thread對(duì)象 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); .... }
這里其實(shí)就是一次jni調(diào)用,把java中的Thread 的nativePeer 進(jìn)行了賦值,而賦值的內(nèi)容,正是通過(guò)了這個(gè)調(diào)用SetLongField
env->SetLongField(peer.get(), WellKnownClasses::java_lang_Thread_nativePeer, reinterpret_cast64<jlong>(self));
這里我們簡(jiǎn)單了解一下SetLongField,如果進(jìn)行過(guò)jni開(kāi)發(fā)的同學(xué)應(yīng)該能過(guò)明白,其實(shí)就是把peer.get()得到的對(duì)象(其實(shí)就是java層的Thread對(duì)象)的nativePeer屬性,賦值為了self(native層的Thread對(duì)象的指針),并強(qiáng)轉(zhuǎn)換為了jlong類(lèi)型。我們接下來(lái)回到j(luò)ava層
Thread.java private volatile long nativePeer;
說(shuō)了一大堆,那么這個(gè)nativePeer究竟是個(gè)什么?通過(guò)上面的代碼分析,我們能夠明白了,Thread.java中的nativePeer就是一個(gè)指針,它所指向的內(nèi)容正是native層中的Thread
nativePeer 與 native Thread tid 與java Thread tid
經(jīng)過(guò)了上面一段落,我們了解了nativePeer,那么我們繼續(xù)對(duì)比一下java層Thread tid 與native層Thread tid。我們通過(guò)在kotlin/java中,調(diào)用Thread對(duì)象的id屬性,其實(shí)得到的是這個(gè)
private long tid;
它的生成方法如下
/* Set thread ID */ tid = nextThreadID();
private static synchronized long nextThreadID() { return ++threadSeqNumber; }
可以看到,雖然它的確能代表一個(gè)java層中Thread的標(biāo)識(shí),但是生成其實(shí)可以看到,他也僅僅是一個(gè)普通的累積id生成,同時(shí)也并沒(méi)有在native層中被當(dāng)作唯一標(biāo)識(shí)進(jìn)行使用。
而native Thread 的 tid屬性,才是真正的線程id
在art中,通過(guò)GetTid獲取
pid_t GetTid() const { return tls32_.tid; }
同時(shí)我們也可以注意到,tid 是保存在 tls32_結(jié)構(gòu)體中,并且其位于Thread對(duì)象的開(kāi)頭,從內(nèi)存分布上看,tid位于state_and_flags、suspend_count、think_lock_thread_id之后,還記得我們上面說(shuō)過(guò)的nativePeer嘛?我們一直強(qiáng)調(diào)native是Thread的指針對(duì)象
因此我們可以通過(guò)指針的偏移,從而算出nativePeer到tid的換算公式,即nativePeer指針向下偏移三位就找到了tid(因?yàn)閟tate_and_flags,state_and_flags,think_lock_thread_id都是int類(lèi)型,那么對(duì)應(yīng)的指針也就是int * )這里有點(diǎn)繞,因?yàn)樯婕爸羔樀膬?nèi)容
int *pInt = reinterpret_cast<int *>(native_peer); //地址 +3,得到tid pInt = pInt + 3; return *pInt;
nativePeer對(duì)象因?yàn)榫驮趈ava層,我們很容易通過(guò)反射就能拿到
val nativePeer = Thread::class.java.getDeclaredField("nativePeer") nativePeer.isAccessible = true val currentNativePeer = nativePeer.get(it)
這里我們通過(guò)nativePeer換算成tid可以寫(xiě)成一個(gè)jni方法
external fun nativePeer2Threadid(nativePeer:Long):Int
實(shí)現(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 強(qiáng)轉(zhuǎn) int int *pInt = reinterpret_cast<int *>(native_peer); //地址 +3,得到 native id pInt = pInt + 3; return *pInt; } } }
dlsym與調(diào)用
我們上面終于把死鎖能涉及到的點(diǎn)都講完,比如如何獲取線程所請(qǐng)求的鎖,當(dāng)前鎖又被那個(gè)線程持有,如何通過(guò)nativePeer獲取Thread id 做了分析,但是還有一個(gè)點(diǎn)我們還沒(méi)能解決,就是如何調(diào)用這些函數(shù)。我們需要調(diào)用的是GetContendedMonitor,GetLockOwnerThreadId,這個(gè)時(shí)候dlsym系統(tǒng)調(diào)用就出來(lái)了,我們可以通過(guò)dlsym 進(jìn)行調(diào)用我們想要調(diào)用的函數(shù)
void* dlsym(void* __handle, const char* __symbol);
這里的symbol是什么呢?其實(shí)我們所有的elf(so也是一種elf文件)的所有調(diào)用函數(shù)都會(huì)生成一個(gè)符號(hào),代表著這個(gè)函數(shù),它在elf的.text中。而我們android中,就會(huì)通過(guò)加載so的方式加載系統(tǒng)庫(kù),加載的系統(tǒng)庫(kù)libart.so里面就包含著我們想要調(diào)用的函數(shù)GetContendedMonitor,GetLockOwnerThreadId的符號(hào)
我們可以通過(guò)objdump -t libart.so 查看符號(hào)
這里我們直接給出來(lái)各個(gè)符號(hào),讀者可以直接用objdump查看符號(hào)
GetContendedMonitor 對(duì)應(yīng)的符號(hào)是
_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE
GetLockOwnerThreadId 對(duì)應(yīng)的符號(hào)
sdk <= 29 _ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE >29是這個(gè) _ZN3art7Monitor20GetLockOwnerThreadIdENS_6ObjPtrINS_6mirror6ObjectEEE
系統(tǒng)限制
然后到這里,我們還是沒(méi)能完成調(diào)用,因?yàn)閐lsym等dl系列的系統(tǒng)調(diào)用,因?yàn)閺腁ndroid 7.0開(kāi)始,Android系統(tǒng)開(kāi)始阻止App中直接使用dlopen(), dlsym()等函數(shù)打開(kāi)系統(tǒng)動(dòng)態(tài)庫(kù),好家伙!谷歌大兄弟為了安全的考慮,做了很多限制。但是這個(gè)防君子不防程序員,業(yè)內(nèi)依舊有很多繞過(guò)系統(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一個(gè)內(nèi)建函數(shù)(通常由編譯器添加),__builtin_return_address(0)用于返回當(dāng)前函數(shù)的返回地址。
在__loader_dlsym 會(huì)進(jìn)行返回地址的校驗(yàn),如果此時(shí)返回地址不是屬于系統(tǒng)庫(kù)的地址,那么調(diào)用就不成功,這也是art虛擬機(jī)保護(hù)手段,因此我們很容易就得出一個(gè)想法,我們是不是可以用系統(tǒng)的某個(gè)函數(shù)去調(diào)用dlsym,然后把結(jié)果給到我們自己的函數(shù)消費(fèi)就可以了?是的,業(yè)內(nèi)已經(jīng)有很多這個(gè)方案了,比如ndk_dlopen
我們拿arm架構(gòu)進(jìn)行分析,arm架構(gòu)中LR寄存器就是保存了當(dāng)前函數(shù)的返回地址,那么我們是不是在調(diào)用dlsym時(shí)可以通過(guò)匯編代碼直接修改LR寄存器的地址為某個(gè)系統(tǒng)庫(kù)的函數(shù)地址就可以了?嗯!是的,但是我們還需要把原來(lái)的LR地址給保存起來(lái),不然就沒(méi)辦法還原原來(lái)的調(diào)用了。
這里我們拿ndk_dlopen的實(shí)現(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)匯編實(shí)現(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寄存器,這個(gè)r2存的東西就是FatalError的地址
- 0x0000000000000008: 13 FF 2F E1 bx r3 ,通過(guò)bx指令調(diào)轉(zhuǎn),就可以正常執(zhí)行我們的dlsym了,r3就是我們自己的dlsym的地址
- 0x0000000000000000: 0C 00 BD E8 pop {r2, r3} 調(diào)用完r3寄存器的方法把r2寄存器放到調(diào)用棧下,提供給后面的執(zhí)行進(jìn)行消費(fèi)
- 0x0000000000000004: 12 FF 2F E1 bx r2 ,最后就回到了我們的r2,完成了一次調(diào)用
總之,我們想要做到dl系列的調(diào)用,就是想盡方法去修改對(duì)應(yīng)架構(gòu)的函數(shù)返回地址的數(shù)值。
死鎖檢測(cè)所有代碼
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、打開(kāi)動(dòng)態(tài)庫(kù)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ù),返回當(dāng)前線程想要競(jìng)爭(zhēng)的monitor int monitorObj = ((int (*)(long)) get_contended_monitor)(native_thread); if (monitorObj != 0) { // 2、獲取這個(gè)monitor被哪個(gè)線程持有,返回該線程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 強(qiáng)轉(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; }
對(duì)應(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é)
我們通過(guò)死鎖這個(gè)例子,去了解了native層Thread的相關(guān)方法,同時(shí)也了解了如何使用dlsym打開(kāi)函數(shù)符號(hào)并調(diào)用。本篇Android性能優(yōu)化就到此結(jié)束,更多關(guān)于Android性能優(yōu)化死鎖監(jiān)控的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android編程實(shí)現(xiàn)獲得手機(jī)屏幕真實(shí)寬高的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)獲得手機(jī)屏幕真實(shí)寬高的方法,以?xún)蓚€(gè)實(shí)例形式分析了獲取手機(jī)屏幕信息的相關(guān)技巧,需要的朋友可以參考下2015-10-10Android實(shí)現(xiàn)qq列表式的分類(lèi)懸浮提示
工作中遇到了一個(gè)需求,讓?xiě)?yīng)用中的一個(gè)列表按照分類(lèi)顯示,并且能提示當(dāng)前是在哪個(gè)分類(lèi),度娘了一番,參考了前輩們的博客后實(shí)現(xiàn)了,現(xiàn)在分享給大家,有需要的可以參考借鑒。2016-09-09Android控件系列之RadioButton與RadioGroup使用方法
本文介紹了Android中如何使用RadioGroup和RadioButton,對(duì)比了RadioButton和CheckBox的區(qū)別,并實(shí)現(xiàn)了自定義的RadioGroup中被選中RadioButton的變更監(jiān)聽(tīng)事件2012-11-11淺談Android設(shè)計(jì)模式學(xué)習(xí)之觀察者模式
觀察者模式在實(shí)際項(xiàng)目中使用的也是非常頻繁的,它最常用的地方是GUI系統(tǒng)、訂閱——發(fā)布系統(tǒng)等。這篇文章主要介紹了淺談Android設(shè)計(jì)模式學(xué)習(xí)之觀察者模式,感興趣的小伙伴們可以參考一下2018-05-05Android Studio 一鍵生成Json實(shí)體類(lèi)教程
這篇文章主要介紹了Android Studio 一鍵生成Json實(shí)體類(lèi)教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04Android無(wú)限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案
這篇文章主要介紹了Android無(wú)限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06Android IntentService詳解及使用實(shí)例
這篇文章主要介紹了Android IntentService詳解及使用實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-03-03使用Android Studio 開(kāi)發(fā)自己的SDK教程
很多時(shí)候我們要將自己開(kāi)發(fā)一個(gè)類(lèi)庫(kù)打包成jar包以供他調(diào)用,這個(gè)jar包也叫你自己的SDK或者叫l(wèi)ibrary。android studio生成jar包的方法與eclipse有所不同。在studio中l(wèi)ibrary其實(shí)是module的概念。2017-10-10Android 監(jiān)聽(tīng)手機(jī)GPS打開(kāi)狀態(tài)實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 監(jiān)聽(tīng)手機(jī)GPS打開(kāi)狀態(tài)實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-05-05Android數(shù)據(jù)類(lèi)型之間相互轉(zhuǎn)換系統(tǒng)介紹
一些初學(xué)Android的朋友可能會(huì)遇到JAVA的數(shù)據(jù)類(lèi)型之間轉(zhuǎn)換的苦惱;本文將為有這類(lèi)需求的朋友解決此類(lèi)問(wèn)題2012-11-11