Android性能優(yōu)化死鎖監(jiān)控知識(shí)點(diǎn)詳解
前言
“死鎖”,這個(gè)從接觸程序開發(fā)的時(shí)候就會(huì)經(jīng)常聽到的詞,它其實(shí)也可以被稱為一種“藝術(shù)”,即互斥資源訪問循環(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)控手段,同時(shí)也希望通過死鎖,去挖掘更加底層的知識(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)依賴的限制(本篇就不涉及了,因?yàn)槲覀冎懒饲皟蓚€(gè)就可以自行分析了)
線程Block狀態(tài)
通過我們對(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)生死鎖(只是有可能),我們可以通過遍歷所有線程,查看是否處于block狀態(tà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
}
通過對(duì)線程的判斷,我們能夠排除大部分非死鎖的線程,那么下一步我們要怎么做呢?如果線程發(fā)生了死鎖,那么一定擁有一個(gè)已經(jīng)持有的互斥資源并且不釋放才有可能造成死鎖對(duì)不對(duì)!那么我們下一步,就是要檢測(cè)當(dāng)前線程所持有的鎖,如果兩個(gè)線程同時(shí)持有對(duì)方所需要的鎖,那么就會(huì)產(chǎn)生死鎖
獲取當(dāng)前線程所請(qǐng)求的鎖
雖然我們?cè)趈ava層沒有相關(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;
}
其中第一步嘗試著通過thread->GetMonitorEnterObject()去拿
mirror::Object* GetMonitorEnterObject() const REQUIRES_SHARED(Locks::mutator_lock_) {
return tlsPtr_.monitor_enter_object;
}
其中tlsPtr_ 其實(shí)就是art虛擬機(jī)中對(duì)于線程ThreadLocal的代表,即代表著只屬于線程的本地對(duì)象,會(huì)先嘗試從這里拿,拿不到的話通過Thread類中的wait_mutex_對(duì)象去拿
Mutex* GetWaitMutex() const LOCK_RETURNED(wait_mutex_) {
return wait_mutex_;
}
GetContendedMonitor 提供了一個(gè)方法查詢當(dāng)前線程想要的鎖對(duì)象,這個(gè)鎖對(duì)象以O(shè)bjPtrmirror::Object對(duì)象表示,其中mirror::Object類型是art中相對(duì)應(yīng)于java層的Object類的代表,我們了解一下即可??吹竭@里我們可能還有一個(gè)疑問,這個(gè)Thread* thread的入?yún)⑹鞘裁茨??(其?shí)是nativePeer,下文我們會(huì)了解)
我們有辦法能夠查詢到線程當(dāng)前請(qǐng)求的鎖,那么這個(gè)鎖被誰持有呢?只有解決這兩個(gè)問題,我們才能進(jìn)行死鎖的判斷對(duì)不對(duì),我們繼續(xù)往下
通過鎖獲取當(dāng)前持有的線程
我們還記得上文中返回的鎖對(duì)象是以O(shè)bjPtrmirror::Object表示的,當(dāng)然,art中同樣提供了方法,讓我們通過這個(gè)鎖對(duì)象去查詢當(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類型表示
注意這里GetLockOwnerThreadId中返回的Thread id千萬不要跟Java層的Thread對(duì)象的tid混淆,這里的tid才是真正的線程id標(biāo)識(shí)
線程啟動(dòng)
我們來看一下native層主線程的啟動(dòng),它隨著art虛擬機(jī)的啟動(dòng)隨即啟動(dòng),我們都知道java層的線程其實(shí)在沒有跟操作系統(tǒng)的線程綁定的時(shí)候,它只能算是一塊內(nèi)存!只要經(jīng)過與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)鍵,為什么主線程稱為“main線程”的原因
soa.Self()->CreatePeer("main", false, runtime->GetMainThreadGroup());
soa.Self()->AssertNoPendingException();
runtime->RunRootClinits(soa.Self());
soa.Self()->NotifyThreadGroup(soa, runtime->GetMainThreadGroup());
soa.Self()->AssertNoPendingException();
}
可以看到,為什么主線程被稱為“主線程”,是因?yàn)樵赼rt虛擬機(jī)啟動(dòng)的時(shí)候,通過CreatePeer函數(shù),創(chuàng)建的名稱是“main”,CreatePeer是native線程中非常重要的存在,所有線程創(chuàng)建都經(jīng)過它,這個(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)容,正是通過了這個(gè)調(diào)用SetLongField
env->SetLongField(peer.get(),
WellKnownClasses::java_lang_Thread_nativePeer,
reinterpret_cast64<jlong>(self));
這里我們簡(jiǎn)單了解一下SetLongField,如果進(jìn)行過jni開發(fā)的同學(xué)應(yīng)該能過明白,其實(shí)就是把peer.get()得到的對(duì)象(其實(shí)就是java層的Thread對(duì)象)的nativePeer屬性,賦值為了self(native層的Thread對(duì)象的指針),并強(qiáng)轉(zhuǎn)換為了jlong類型。我們接下來回到j(luò)ava層
Thread.java private volatile long nativePeer;
說了一大堆,那么這個(gè)nativePeer究竟是個(gè)什么?通過上面的代碼分析,我們能夠明白了,Thread.java中的nativePeer就是一個(gè)指針,它所指向的內(nèi)容正是native層中的Thread

nativePeer 與 native Thread tid 與java Thread tid
經(jīng)過了上面一段落,我們了解了nativePeer,那么我們繼續(xù)對(duì)比一下java層Thread tid 與native層Thread tid。我們通過在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í)也并沒有在native層中被當(dāng)作唯一標(biāo)識(shí)進(jìn)行使用。
而native Thread 的 tid屬性,才是真正的線程id

在art中,通過GetTid獲取
pid_t GetTid() const {
return tls32_.tid;
}
同時(shí)我們也可以注意到,tid 是保存在 tls32_結(jié)構(gòu)體中,并且其位于Thread對(duì)象的開頭,從內(nèi)存分布上看,tid位于state_and_flags、suspend_count、think_lock_thread_id之后,還記得我們上面說過的nativePeer嘛?我們一直強(qiáng)調(diào)native是Thread的指針對(duì)象

因此我們可以通過指針的偏移,從而算出nativePeer到tid的換算公式,即nativePeer指針向下偏移三位就找到了tid(因?yàn)閟tate_and_flags,state_and_flags,think_lock_thread_id都是int類型,那么對(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層,我們很容易通過反射就能拿到
val nativePeer = Thread::class.java.getDeclaredField("nativePeer")
nativePeer.isAccessible = true
val currentNativePeer = nativePeer.get(it)
這里我們通過nativePeer換算成tid可以寫成一個(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è)線程持有,如何通過nativePeer獲取Thread id 做了分析,但是還有一個(gè)點(diǎn)我們還沒能解決,就是如何調(diào)用這些函數(shù)。我們需要調(diào)用的是GetContendedMonitor,GetLockOwnerThreadId,這個(gè)時(shí)候dlsym系統(tǒng)調(diào)用就出來了,我們可以通過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ì)通過加載so的方式加載系統(tǒng)庫(kù),加載的系統(tǒng)庫(kù)libart.so里面就包含著我們想要調(diào)用的函數(shù)GetContendedMonitor,GetLockOwnerThreadId的符號(hào)
我們可以通過objdump -t libart.so 查看符號(hào)

這里我們直接給出來各個(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)限制
然后到這里,我們還是沒能完成調(diào)用,因?yàn)閐lsym等dl系列的系統(tǒng)調(diào)用,因?yàn)閺腁ndroid 7.0開始,Android系統(tǒng)開始阻止App中直接使用dlopen(), dlsym()等函數(shù)打開系統(tǒng)動(dòng)態(tài)庫(kù),好家伙!谷歌大兄弟為了安全的考慮,做了很多限制。但是這個(gè)防君子不防程序員,業(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一個(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í)可以通過匯編代碼直接修改LR寄存器的地址為某個(gè)系統(tǒng)庫(kù)的函數(shù)地址就可以了?嗯!是的,但是我們還需要把原來的LR地址給保存起來,不然就沒辦法還原原來的調(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 ,通過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、打開動(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é)
我們通過死鎖這個(gè)例子,去了解了native層Thread的相關(guān)方法,同時(shí)也了解了如何使用dlsym打開函數(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í)寬高的方法,以兩個(gè)實(shí)例形式分析了獲取手機(jī)屏幕信息的相關(guān)技巧,需要的朋友可以參考下2015-10-10
Android實(shí)現(xiàn)qq列表式的分類懸浮提示
工作中遇到了一個(gè)需求,讓應(yīng)用中的一個(gè)列表按照分類顯示,并且能提示當(dāng)前是在哪個(gè)分類,度娘了一番,參考了前輩們的博客后實(shí)現(xiàn)了,現(xiàn)在分享給大家,有需要的可以參考借鑒。2016-09-09
Android控件系列之RadioButton與RadioGroup使用方法
本文介紹了Android中如何使用RadioGroup和RadioButton,對(duì)比了RadioButton和CheckBox的區(qū)別,并實(shí)現(xiàn)了自定義的RadioGroup中被選中RadioButton的變更監(jiān)聽事件2012-11-11
淺談Android設(shè)計(jì)模式學(xué)習(xí)之觀察者模式
觀察者模式在實(shí)際項(xiàng)目中使用的也是非常頻繁的,它最常用的地方是GUI系統(tǒng)、訂閱——發(fā)布系統(tǒng)等。這篇文章主要介紹了淺談Android設(shè)計(jì)模式學(xué)習(xí)之觀察者模式,感興趣的小伙伴們可以參考一下2018-05-05
Android Studio 一鍵生成Json實(shí)體類教程
這篇文章主要介紹了Android Studio 一鍵生成Json實(shí)體類教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-04-04
Android無限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案
這篇文章主要介紹了Android無限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
Android IntentService詳解及使用實(shí)例
這篇文章主要介紹了Android IntentService詳解及使用實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-03-03
使用Android Studio 開發(fā)自己的SDK教程
很多時(shí)候我們要將自己開發(fā)一個(gè)類庫(kù)打包成jar包以供他調(diào)用,這個(gè)jar包也叫你自己的SDK或者叫l(wèi)ibrary。android studio生成jar包的方法與eclipse有所不同。在studio中l(wèi)ibrary其實(shí)是module的概念。2017-10-10
Android 監(jiān)聽手機(jī)GPS打開狀態(tài)實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 監(jiān)聽手機(jī)GPS打開狀態(tài)實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-05-05
Android數(shù)據(jù)類型之間相互轉(zhuǎn)換系統(tǒng)介紹
一些初學(xué)Android的朋友可能會(huì)遇到JAVA的數(shù)據(jù)類型之間轉(zhuǎn)換的苦惱;本文將為有這類需求的朋友解決此類問題2012-11-11

