深入分析Android|深入分析Android Handler中的知识点
Handler发送Message流程
- 从主线程
new Handler()
开始:
public Handler() {
this(null, false);
}public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
// 获取当前线程的looper
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
// 获取当前线程的messageQueue
mQueue = mLooper.mQueue;
mCallback = callback;
// 回调
mAsynchronous = async;
// 设置异步标记,异步消息会优先处理
}
// 通过ThreadLocal保证每个线程只有一个looper
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
- 主线程的
Looper
是在应用程序启动时ActivityThread
为我们创默认创建了,所以上面new Handler()
中可以直接获取到主线程的Looper
:
public static void main(String[] args) {
...
// 构造主线程Looper对象
Looper.prepareMainLooper();
...
...
// 应用运行期间所有代码都运行在此位置,如果主线程Looper退出循环,则应用运行结束// 开启消息循环
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
-
Looper
初始化和准备过程
public static void prepareMainLooper() {
// 创建实例
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
// 变量赋值
sMainLooper = myLooper();
}
}private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {// 一个线程只能有一个Looper
throw new RuntimeException("Only one Looper may be created per thread");
}
// new了一个Looper对象
sThreadLocal.set(new Looper(quitAllowed));
}// 开始死循环获取消息
public static void loop() {
final Looper me = myLooper();
// 获取当前线程looper
if (me == null) {
throw new RuntimeException("No Looper;
Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// 获取到当前消息队列// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
// 开启死循环轮询消息
for (;
;
) {
// 从消息队列中获取下一个未处理的消息,重点,后面分析
Message msg = queue.next();
// might block
// 消息队列为空,没有消息了,此时说明退出应用了
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}...
try {
// 消息分发回调给handler处理
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}
...
// 回收消息加入复用池
msg.recycleUnchecked();
}
}
-
new Handler()
以及主线程Looper
的初始化和准备工作已经完成,接下去就是常见的发送消息
// Message的复用机制下面再分析
handler.sendMessage(Message.obtain());
// 发送消息
public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}// 发送消息如果不设置延时,内部设置为0
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
// 时间的基数为当前系统启动时间,即使delay=0,实际上uptimeMillis也是>0
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}// 不论是post,sendEmptyMessage等最终都是走到这里
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
// 当前线程消息队列
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
// 将消息按时间放入队列中
return enqueueMessage(queue, msg, uptimeMillis);
}// 将消息添加到消息队列
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
// target赋值给当前handler,所以message持有当前handler的引用,一般情况new Handler匿名内部类持有外部Activity引用,而MessageQueue持有Message引用,从而产生生命周期比Activity长的引用链,因此会导致Activity内存泄露(普通匿名内部类一般不会导致内存泄露)
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
// 如果是异步消息,设置标记位FLAG_ASYNCHRONOUS
if (mAsynchronous) {
msg.setAsynchronous(true);
}
// 最终调用MessageQueue的enqueueMessage方法,将当前消息放到队列的尾部
return queue.enqueueMessage(msg, uptimeMillis);
}
- 接下去就是将消息放入消息队列,等待消息的轮询处理
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
// 注意,同步锁!为了处理多个线程中Handler发送消息的并发处理
synchronized (this) {
// 当前Looper正在执行quit操作,也是调用MessageQueue的quit,此时发送消息没必要处理了
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
// 回收消息
msg.recycle();
return false;
}msg.markInUse();
// 标记当前消息正在处理
msg.when = when;
// 当前消息的需处理时间
// ----->>>> 重点:消息入队及排序
Message p = mMessages;
// 当前消息队头
boolean needWake;
// 是否需要唤醒,一般不需要用户去主动去唤醒,除非无消息了长期休眠
// 1. p = null说明当前队列没有消息,直接插入到队头(通常这种场景)
// 2. when==0只有Handler调用sendMessageAtFrontOfQueue时成立
// 3. when < p.when说明当前消息时间小于队头消息的时间,需要插到最前面
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
// msg插入到队头:msg->p->...
mMessages = msg;
// 刷新当前队头
needWake = mBlocked;
// 上次取消息时,队列没有消息了,mBlocked=true并且挂起,所以此刻需要唤醒
} else {// 当前队列中已经存在其他未处理消息,mBlocked=false
// Inserted within the middle of the queue.Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
// 同步屏障消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
// 一般是false
Message prev;
// 定义上一个消息
for (;
;
) {// 循环遍历
prev = p;
// 将当前队头赋值给上个消息
p = p.next;
// 将之前的队头的下一个消息赋值给p
// 1. msg1 -> msg2(假设当前队列中有两个消息,msg1是当前队头)
// 2. 定义一个变量p = msg1
// 3. 定义一个变量pre = p, 也就是pre = msg1
// 4. p = msg1, p.next = msg1.next = msg2, 所以p = msg2
// 5. 此时消息顺序: pre -> p
// 6. 不断循环上述操作,pre和p一直往后移动:pre是倒数第二个,p是队尾
if (p == null || when < p.when) {
// p==null说明已经是队列的尾部了
// whenmsg->p || pre->msg->p->...
msg.next = p;
// invariant: p == prev.next
prev.next = msg;
}// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
// native层唤醒,停止next方法中的阻塞nativePollOnce,立刻获取消息
nativeWake(mPtr);
}
}
return true;
}
- 正常情况下至此,消息已经发送并且成功加入到
MessageQueue
的队尾了,接下去就是消息的轮询处理了,上面已经提到了Looper.loop()
方法,内部是个死循环,不断调用MessageQueue.next()
去获取下一个消息
// 开启死循环轮询消息
for (;
;
) {
// 从消息队列中获取下一个未处理的消息,重点,后面分析
Message msg = queue.next();
// might block
// 消息队列为空,没有消息了,此时说明退出应用了
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}...
try {
// 消息分发回调给handler处理
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}
...
// 回收消息加入复用池
msg.recycleUnchecked();
}
// 获取下一个消息
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
// native层的MessageQueue的指针
if (ptr == 0) {
return null;
// native退出了或者挂了,epoll机制已经无效了,还玩啥?
}
// 这个值会影响下面的mBlocked标记
int pendingIdleHandlerCount = -1;
// -1 only during first iteration
int nextPollTimeoutMillis = 0;
// 同样是个死循环
for (;
;
) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// 此处可能会阻塞,但是不会导致应用卡死(应用卡死是ANR)
// 阻塞过程会休眠cpu(linux机制),节约系统资源
// 阻塞不会一直延续,会在超时后自动唤醒,touch事件或者调用nativeWait会主动唤醒休眠
nativePollOnce(ptr, nextPollTimeoutMillis);
// 注意,消息入队同步锁,取消息同样有同步锁,都是为了处理多线程并发问题
// 此处也说明当前队列的消息并不是完全准确的按照delayTime处理,可能存在延时
synchronized (this) {
// Try to retrieve the next message.Return if found.
final long now = SystemClock.uptimeMillis();
// 当前系统启动时间
Message prevMsg = null;
// 定义上一个消息prevMsg
Message msg = mMessages;
// 当前消息队头
// msg.target==null说明是postSyncBarrier方式发送的同步屏障消息,非一般通过handler的sendMessage发送出来
if (msg != null && msg.target == null) {
// Stalled by a barrier.Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
// 不断往后遍历,msg为当前遍历的队尾,直到找到最前面的异步消息
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {// msg为当前队头或者异步消息
if (now < msg.when) {// 队头消息的时间都还没到,暂时不需要处理,计算下一次唤醒时间
// Next message is not ready.Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
// 计算本次阻塞到下次唤醒的时长
} else {// 到时间了,需要取出消息处理
// Got a message.
mBlocked = false;
// 说明有消息存在,不需要阻塞
if (prevMsg != null) {// 同步屏障消息的场景,msg为最靠前的异步消息
prevMsg.next = msg.next;
// 取出了队列中间最前的异步消息,重新链接队列链表
} else {
mMessages = msg.next;
// 之前的队头已经取出来处理了,队头后移一个
}
msg.next = null;
// msg已经从队列中取出
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
// 标记msg正在使用
return msg;
// 取出需要处理的消息,返回
}
} else {// msg为空,没有消息了,需要一直挂起
// No more messages.
nextPollTimeoutMillis = -1;
}// Process the quit message now that all pending messages have been handled.
if (mQuitting) {// 正在退出,返回null
dispose();
return null;
}// 假设上面没有return message,说明当前队列没有需要处理的消息(没有消息或者需要处理的时间未到),则开始执行idleHandler
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0 // 上面初始值为-1
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
// 获取idleHandlers容量
}
if (pendingIdleHandlerCount <= 0) {// 没有idleHandler需要执行
// No idle handlers to run.Loop and wait some more.
mBlocked = true;
// 走到这说明没有return message,也没有idleHandler需要执行,所以要阻塞
// 此时nextPollTimeoutMillis = -1会一直阻塞直到被主动唤醒
continue;
}
// 初始化idleHandlers
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// 执行空闲idleHandler
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0;
i < pendingIdleHandlerCount;
i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null;
// release the reference to the handlerboolean keep = false;
try {
keep = idler.queueIdle();
// 是否一次性的标志,具体执行
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
// 一次性执行后移除
}
}
}// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// 上面已经执行过了idleHandler,所以赋值0,所以for死循环中不会再执行// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
// idleHandler执行后立即唤醒,可能有延时消息到了处理时机
}
}
- 取出消息后,再回去看
Looper.loop()
里对消息的分发
public static void loop() {
...
for (;
;
) {
Message msg = queue.next();
// might block
if (msg == null) {// 没消息说明当前已经调用了quit退出循环
// No message indicates that the message queue is quitting.
return;
}...try {
msg.target.dispatchMessage(msg);
// msg的target就是Handler,回调handler的dispatchMessage方法
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...msg.recycleUnchecked();
}
}public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
// 如果msg有自己的callback,执行msg的callback,一般情况下callback都为null
} else {
if (mCallback != null) {// 这个mCallback对于app来说是无法使用的,google已经添加了注解@UnsupportedAppUsage
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
// 所以,通常情况就走到这里了,回调消息处理
}
}// Message.obtain方法有一个重载,内部有两个参数:
// public static Message obtain(Handler h, Runnable callback) {...}
private static void handleCallback(Message message) {
message.callback.run();
// 执行callback的run方法
}
- 至此,整个消息分发处理流程已经分析结束。总结如下:
-
Looper.prepare
->new Looper
对象并添加到ThreadLocal
保存 -
Looper
构造函数中 ->new MessageQueue
创建消息队列 -
Looper.loop
-> 开启死循环轮询,不断调用MessageQueue.next
-
MessageQueue.next()
获取下一条需要处理的消息,无消息或者还未到处理时间则阻塞休眠 -
Handler.sendMessage
->Handler.enqueueMessage
->MessageQueue.enqueueMessage
将消息添加到队列 -
Looper.loop
循环中通过MessageQueue.next
取出发送的消息 ->Msg
-
Msg.target(Handler).dispatchMessage
->Handler.handleMessage
- 当消息队列没有消息时会持续挂起,下一条消息来了会触发主动唤醒
- 正常情况下系统会根据当前时间和队头消息的处理时间计算出下次唤醒的时间,不需要主动触发唤醒
-
Message
是可以通过new
的方式来进行创建,但是该方式就会在堆区申请内存空间,android
系统内部充斥着大量消息,如果每个消息都通过该方式进行创建,该消息处理完后会产生大量的垃圾碎片,造成内存抖动,频繁gc
,严重影响性能。因此,内部通过一个缓存池进行复用(链表结构)
public static final Object sPoolSync = new Object();
// 同步对象锁
private static Message sPool;
// 消息队列中的队头(也可以称为缓存池,message自身是链表结构,next指向下一条消息)
private static int sPoolSize = 0;
// 当前缓存池大小
private static final int MAX_POOL_SIZE = 50;
// 最多缓存50个消息
@UnsupportedAppUsage
/*package*/ Message next;
// 享元设计模式,对象共享,只是擦除内部属性
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {// 同步处理
if (sPool != null) {// 缓存池为空
// 从message链表结构中取出队头的message给外部使用,同时将sPool指向新的队头
Message m = sPool;
sPool = m.next;
m.next = null;
// 擦除内部标记,断开消息链
m.flags = 0;
// clear in-use flag
sPoolSize--;
return m;
// 返回该消息对象
}
}
return new Message();
}// sPool缓存池的创建初始化,系统调用
/**
* Recycles a Message that may be in-use.
* Used internally by the MessageQueue and Looper when disposing of queued Messages.
*/
@UnsupportedAppUsage
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = https://www.it610.com/article/null;
synchronized (sPoolSync) {// 同步处理
if (sPoolSize < MAX_POOL_SIZE) {// 当前容量小于最大容量50
next = sPool;
// sPool赋值给next
sPool = this;
// 当前消息赋值给sPool:sPool->当前msg->next... 下次复用的永远是队头的消息
sPoolSize++;
// 容量+1
}
}
}
Looper线程单例原理
【深入分析Android|深入分析Android Handler中的知识点】核心类就是
ThreadLocal
,它提供线程局部变量,每个线程都有自己独立的一份变量,通常是类中的 private static
字段,它们希望将状态与某一个线程相关联,在多线程编程中常用,比如Android
的绘制同步机制Choreographer
中也有使用。
@UnsupportedAppUsage
static final ThreadLocal sThreadLocal = new ThreadLocal();
// 会判断,如果之前ThreadLocal已经存在Looper对象,抛出异常,一个线程只能有一个Looper
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程
ThreadLocalMap map = getMap(t);
// 获取当前线程的ThreadLocalMap,默认为null
if (map != null)// 当前线程已经初始化了ThreadLocalMap
map.set(this, value);
// key为当前ThreadLoacl
else
createMap(t, value);
// 延迟,第一次set时候才进行初始化
}void createMap(Thread t, T firstValue) {
// ThreadLocalMap和当前线程绑定,保证线程唯一性
t.threadLocals = new ThreadLocalMap(this, firstValue);
// 创建ThreadLocalMap,并且添加初始值
}static class ThreadLocalMap {
// 构造函数会存储第一次赋值
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
// 存储的值Entry(ThreadLocal> k, Object v) {
super(k);
// 弱引用缓存当前ThreadLocal对象
value = https://www.it610.com/article/v;
// 存储的值
}//Entry.get() == null表示key不再被引用,表示ThreadLocal对象被回收
}private Entry[] table;
// 缓存key-value的数组// setter赋值
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
// 缓存数组
int len = tab.length;
// 当前容量
int i = key.threadLocalHashCode & (len-1);
// 生成索引,新增一个就会变化for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
if (k == key) {// 当前ThreadLocal对象为key,如果相等覆盖之前的value值
e.value = https://www.it610.com/article/value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
// 加入缓存
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz>= threshold)
rehash();
}// getter取值
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
// 生成索引,规则同set方法
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
}public T get() {
Thread t = Thread.currentThread();
// 获取当前线程
ThreadLocalMap map = getMap(t);
// ThreadLocalMap和线程Thread唯一对应,所以get操作只有当前线程可以访问
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
// 根据key获取entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// 取出value值
return result;
}
}
return setInitialValue();
}
- 小结一下set和get流程,里面具体的hash映射算法和索引计算法未分析,看不懂(线性探测法寻址)
-
ThreadLocal.set()
->getMap() or createMap()
返回当前Thread
的ThreadLocalMap
- 当前
Thread
的ThreadLocalMap.put(ThreadLocal, value)
存入数据,其中key
就是ThreadLoacal
-
ThreadLocal.get()
->getMap()
返回当前Thread
的ThreadLocalMap
- 当前
Thread
的ThreadLocalMap.get(ThreadLocal)
读取数据
-
ThreadLocal
使用不当会出现内存泄露,出现内存泄露需同时满足以下三个条件:
-
ThreadLocal
引用被设置为null
,且后面没有set,get,remove
操作,该entry
变成游离状态 - 线程一直运行,不停止(线程池)
- 触发了垃圾回收(
Minor GC
或Full GC
)
-
Android Looper
中并没有调用ThreadLocal
的remove
,为何不会出现内存泄露呢?主要有以下原因:
Looper
中的ThreadLocal
使用static final
修饰,static
修饰的生命周期与Application
同在,Application
退出时线程自然停止运行了,并且final
修饰其他地方无法修改其引用。因此同时打破了上面的条件1,2,不会出现ThreadLocalMap
存储数组中key
为null
时触发GC
的内存泄露问题
Handler
引申出来的知识点包括以下:-
Handler
发送消息流程,与Looper,MessageQueue
三者之间角色关系 -
Handler
内存泄露原因以及处理方案 -
MessageQueue
数据结构,链表和队列区别,Message
入队和出队的遍历方法 -
Looper
如何保证线程变量唯一,ThreadLocal
原理和内存泄露 -
ThreadLocal
内存泄露引申出弱引用,软引用和强引用 -
Message
消息的复用机制,缓存原理,链表结构 -
Message
设计复用机制的原因,内存抖动,享元设计模式 - 消息屏障
Barrier
是如何保证优先执行的,以及系统内部应用场景 -
Android
中应用卡死的定义,ANR -
MessageQueue
死循环节约资源处理方案,linux
的ePoll
机制 -
IdleHandler
的作用,和处理时机,处理方式
推荐阅读
- android第三方框架(五)ButterKnife
- Android中的AES加密-下
- 带有Hilt的Android上的依赖注入
- android|android studio中ndk的使用
- Android事件传递源码分析
- RxJava|RxJava 在Android项目中的使用(一)
- Android7.0|Android7.0 第三方应用无法访问私有库
- 深入理解|深入理解 Android 9.0 Crash 机制(二)
- android防止连续点击的简单实现(kotlin)
- Android|Android install 多个设备时指定设备