Handler消息机制:面试初级、中级、高级问法,你都掌握了吗?

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

// do this last because it fires off messages to start doing things
try {
//ViewRootImpl
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}

setView内部会通过requestLayout来完成异步刷新请求,同时也会调用checkThread方法来检验线程的合法性。

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

因此,我们的ViewRootImpl的创建是在子线程,所以mThread的值也是子线程,同时我们的更新也是在子线程,所以不会产生异常,同时也可以参考这篇文章分析,写的非常详细。同理下面的代码也可以验证这个情况

//子线程中调用
public void showDialog(){
new Thread(new Runnable() {
@Override
public void run() {
//创建Looper,MessageQueue
Looper.prepare();
new Handler().post(new Runnable() {
@Override
public void run() {
builder = new AlertDialog.Builder(HandlerActivity.this);
builder.setTitle(“jackie”);
alertDialog = builder.create();
alertDialog.show();
alertDialog.hide();
}
});
//开始处理消息
Looper.loop();
}
}).start();
}

在子线程中调用showDialog方法,先调用alertDialog.show()方法,再调用alertDialog.hide()方法,hide方法只是将Dialog隐藏,并没有做其他任何操作(没有移除Window),然后再在主线程调用alertDialog.show();便会抛出Only the original thread that created a view hierarchy can touch its views异常了。

2020-11-09 18:35:39.874 24819-24819/com.jackie.testdialog E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.jackie.testdialog, PID: 24819
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8191)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1420)
at android.view.View.requestLayout(View.java:24454)
at android.view.View.setFlags(View.java:15187)
at android.view.View.setVisibility(View.java:10836)
at android.app.Dialog.show(Dialog.java:307)
at com.jackie.testdialog.HandlerActivity$2.onClick(HandlerActivity.java:41)
at android.view.View.performClick(View.java:7125)
at android.view.View.performClickInternal(View.java:7102)

所以在线程中更新UI的重点是创建它的ViewRootImpl和checkThread所在的线程是否一致

如何在主线程中访问网络

在网络请求之前添加如下代码

StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitNetwork().build();
StrictMode.setThreadPolicy(policy);

StrictMode(严苛模式)Android2.3引入,用于检测两大问题:ThreadPolicy(线程策略)和VmPolicy(VM策略),这里把严苛模式的网络检测关了,就可以在主线程中执行网络操作了,一般是不建议这么做的。关于严苛模式可以查看这里

系统为什么不建议在子线程中访问UI?

这是因为 Android 的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:

  1. 首先加上锁机制会让UI访问的逻辑变得复杂

  2. 锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。

所以最简单且高效的方法就是采用单线程模型来处理UI操作。(安卓开发艺术探索)

子线程如何通知主线程更新UI(都是通过Handle发送消息到主线程操作UI的)
  1. 主线程中定义 Handler,子线程通过 mHandler 发送消息,主线程 Handler 的 handleMessage 更新UI。
  2. 用 Activity 对象的 runOnUiThread 方法。
  3. 创建 Handler,传入 getMainLooper。
  4. View.post(Runnable r) 。
Looper死循环为什么不会导致应用卡死,会耗费大量资源吗?

从前面的主线程、子线程的分析可以看出,Looper会在线程中不断的检索消息,如果是子线程的Looper死循环,一旦任务完成,用户应该手动退出,而不是让其一直休眠等待。(引用自Gityuan)线程其实就是一段可执行的代码,当可执行的代码执行完成后,线程的生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder 线程也是采用死循环的方法,通过循环方式不同与 Binder 驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。Android是基于消息处理机制的,用户的行为都在这个Looper循环中,我们在休眠时点击屏幕,便唤醒主线程继续进行工作

主线程的死循环一直运行是不是特别消耗 CPU 资源呢? 其实不然,这里就涉及到 Linux pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

主线程的Looper何时退出

在App退出时,ActivityThread中的mH(Handler)收到消息后,执行退出。

//ActivityThread.java
case EXIT_APPLICATION:
if (mInitialApplication != null) {
mInitialApplication.onTerminate();
}
Looper.myLooper().quit();
break;

如果你尝试手动退出主线程Looper,便会抛出如下异常

Caused by: java.lang.IllegalStateException: Main thread not allowed to quit.
at android.os.MessageQueue.quit(MessageQueue.java:428)
at android.os.Looper.quit(Looper.java:354)
at com.jackie.testdialog.Test2Activity.onCreate(Test2Activity.java:29)
at android.app.Activity.performCreate(Activity.java:7802)
at android.app.Activity.performCreate(Activity.java:7791)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread H . h a n d l e M e s s a g e ( A c t i v i t y T h r e a d . j a v a : 2016 ) a t a n d r o i d . o s . H a n d l e r . d i s p a t c h M e s s a g e ( H a n d l e r . j a v a : 107 ) a t a n d r o i d . o s . L o o p e r . l o o p ( L o o p e r . j a v a : 214 ) a t a n d r o i d . a p p . A c t i v i t y T h r e a d . m a i n ( A c t i v i t y T h r e a d . j a v a : 7356 ) a t j a v a . l a n g . r e f l e c t . M e t h o d . i n v o k e ( N a t i v e M e t h o d ) a t c o m . a n d r o i d . i n t e r n a l . o s . R u n t i m e I n i t H.handleMessage(ActivityThread.java:2016) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit H.handleMessage(ActivityThread.java:2016)atandroid.os.Handler.dispatchMessage(Handler.java:107)atandroid.os.Looper.loop(Looper.java:214)atandroid.app.ActivityThread.main(ActivityThread.java:7356)atjava.lang.reflect.Method.invoke(NativeMethod)atcom.android.internal.os.RuntimeInitMethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

为什么不允许退出呢,因为主线程不允许退出,一旦退出就意味着程序挂了,退出也不应该用这种方式退出

Handler的消息处理顺序

在Looper执行消息循环loop()时会执行下面这行代码,msg.targe就是这个Handler对象

msg.target.dispatchMessage(msg);

我们来看看dispatchMessage的源码

public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
//如果 callback 处理了该 msg 并且返回 true, 就不会再回调 handleMessage
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

  1. 如果Message这个对象有CallBack回调的话,这个CallBack实际上是个Runnable,就只执行这个回调,然后就结束了,创建该Message的CallBack代码如下:

Message msgCallBack = Message.obtain(handler, new Runnable() {
@Override
public void run() {
}
});

而handleCallback方法中调用的是Runnable的run方法。

private static void handleCallback(Message message) {
message.callback.run();
}

  1. 如果Message对象没有CallBack回调,进入else分支判断Handler的CallBack是否为空,不为空执行CallBack的handleMessage方法,然后return,构建Handler的CallBack代码如下:

Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
//retrun true,就不执行下面的逻辑了,可以用于做优先级的处理
return false;
}
};

  1. 最后才调用到Handler的handleMessage()函数,也就是我们经常去重写的函数,在该方法中做消息的处理。
使用场景

可以看到Handler.Callback 有优先处理消息的权利 ,当一条消息被 Callback 处理并拦截(返回 true),那么 Handler 的 handleMessage(msg) 方法就不会被调用了;如果 Callback 处理了消息,但是并没有拦截,那么就意味着一个消息可以同时被 Callback 以及 Handler 处理。我们可以利用CallBack这个拦截来拦截Handler的消息

场景:Hook ActivityThread.mH , 在 ActivityThread 中有个成员变量 mH ,它是个 Handler,又是个极其重要的类,几乎所有的插件化框架都使用了这个方法。

Handler.post(Runnable r)方法的执行逻辑

我们需要分析平时常用的Handler.post(Runnable r)方法是如何执行的,是否新创建了一个线程了呢,实际上并没有,这个Runnable对象只是被调用了它的run方法,根本并没有启动一个线程,源码如下:

//Handler.java
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage®, 0);
}

private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

最终该Runnable对象被包装成一个Message对象,也就是这个Runnable对象就是该Message的CallBack对象了,有优先执行的权利了。

Handler是如何进行线程切换的

原理很简单,线程间是共享资源的,子线程通过handler.sendXXXhandler.postXXX等方法发送消息,然后通过Looper.loop()在消息队列中不断的循环检索消息,最后交给handle.dispatchMessage方法进行消息的分发处理。

如何处理Handler使用不当造成的内存泄漏?
  1. 有延时消息,在界面关闭后及时移除Message/Runnable,调用handler.removeCallbacksAndMessages(null)

  2. 内部类导致的内存泄漏改为静态内部类,并对上下文或者Activity/Fragment使用弱引用。

具体内存泄漏的分析和解决可以参考这篇文章。同时还有一个很关键的点,如果有个延时消息,当界面关闭时,该Handler中的消息还没有处理完毕,那么最终这个消息是怎么处理的?经过测试,比如我打开界面后延迟10s发送消息,关闭界面,最终在Handler(匿名内部类创建的)的handMessage方法中还是会收到消息(打印日志)。因为会有一条MessageQueue -> Message -> Handler -> Activity的引用链,所以Handler不会被销毁,Activity也不会被销毁。

正确创建Message实例
  1. 通过 Message 的静态方法 Message.obtain() 获取;
  2. 通过 Handler 的公有方法 handler.obtainMessage()

所有的消息会被回收,放入sPool中,使用享元设计模式。

Handler深层次问题解答
ThreadLocal

ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

如果让你设计一个ThreadLocal,ThreadLocal 的目标是让不同的线程有不同的变量 V,那最直接的方法就是创建一个 Map,它的 Key 是线程,Value 是每个线程拥有的变量 V,ThreadLocal 内部持有这样的一个 Map 就可以了。你可能会设计成这样

实际上Java的实现是下面这样,Java 的实现里面也有一个 Map,叫做 ThreadLocalMap,不过持有 ThreadLocalMap 的不是 ThreadLocal,而是 Thread。Thread 这个类内部有一个私有属性 threadLocals,其类型就是 ThreadLocalMap,ThreadLocalMap 的 Key 是 ThreadLocal。

精简之后的代码如下

class Thread {
//内部持有ThreadLocalMap
ThreadLocal.ThreadLocalMap
threadLocals;
}
class ThreadLocal{
public T get() {
//首先获取线程持有的
//ThreadLocalMap
ThreadLocalMap map =
Thread.currentThread()
.threadLocals;
//在ThreadLocalMap中
//查找变量
Entry e =
map.getEntry(this);
return e.value;
}
static class ThreadLocalMap{
//内部是数组而不是Map
Entry[] table;
//根据ThreadLocal查找Entry
Entry getEntry(ThreadLocal key){
//省略查找逻辑
}
//Entry定义
static class Entry extends
WeakReference{
Object value;
}
}
}

在Java的实现方案中,ThreadLocal仅仅只是一个代理工具类,内部并不持有任何线程相关的数据,所有和线程相关的数据都存储在Thread里面,这样的设计从数据的亲缘性上来讲,ThreadLocalMap属于Thread也更加合理。所以ThreadLocal的get方法,其实就是拿到每个线程独有的ThreadLocalMap

还有一个原因,就是不容易产生内存泄漏,如果用我们的设计方案,ThreadLocal持有的Map会持有Thread对象的引用,这就意味着只要ThreadLocal对象存在,那么Map中的Thread对象就永远不会被回收。ThreadLocal的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄漏。

而Java的实现中Thread持有ThreadLocalMap,而且ThreadLocalMap里对ThreadLocal的引用还是弱引用,所以只要Thread对象可以被回收,那么ThreadLocalMap就能被回收。Java的实现方案虽然看上去复杂一些,但是更安全

ThreadLocal与内存泄漏

但是一切并不总是那么完美,如果在线程池中使用ThreadLocal可能会导致内存泄漏,原因是线程池中线程的存活时间太长,往往和程序都是同生共死的,这就意味着Thread持有的ThreadLocalMap一直都不会被回收,再加上ThreadLocalMap中的Entry对ThreadLocal是弱引用,所以只要ThreadLocal结束了自己的生命周期是可以被回收掉的。但是Entry中的Value却是被Entry强引用的,所以即便Value的生命周期结束了,Value也是无法被回收的,从而导致内存泄漏

所以我们可以通过try{}finally{}方案来手动释放资源

ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
}finally {
//手动清理ThreadLocal
tl.remove();
}
});

以上ThreadLocal内容主要参考自这里

epoll机制

epoll机制在Handler中的应用,在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,最终调用到epoll_wait()进行阻塞等待。此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

这里有一篇深度好文聊聊IO多路复用之select,poll,epoll详解,这边拿文章中的最后两段话:

  1. 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

  2. select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

之所以选择Handler底层选择epoll机制,我感觉是epoll在效率上更高。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

Handler的同步屏障机制

如果有一个紧急的Message需要优先处理,该怎么做?这其实涉及到架构方面的设计了,通用场景和特殊场景的设计。你可能会想到sendMessageAtFrontOfQueue()这个方法,实际也远远不只是如此,Handler中加入了同步屏障这种机制,来实现[异步消息优先]执行的功能

postSyncBarrier()发送同步屏障,removeSyncBarrier()移除同步屏障

同步屏障的作用可以理解成拦截同步消息的执行,主线程的 Looper 会一直循环调用 MessageQueue 的 next() 来取出队头的 Message 执行,当 Message 执行完后再去取下一个。当 next() 方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next() 方法陷入阻塞状态。如果 next() 方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除出队列,否则主线程就一直不会去处理同步屏幕后面的同步消息。

而所有消息默认都是同步消息,只有手动设置了异步标志,这个消息才会是异步消息。另外,同步屏障消息只能由内部来发送,这个接口并没有公开给我们使用。

Choreographer 里所有跟 message 有关的代码,你会发现,都手动设置了异步消息的标志,所以这些操作是不受到同步屏障影响的。这样做的原因可能就是为了尽可能保证上层 app 在接收到屏幕刷新信号时,可以在第一时间执行遍历绘制 View 树的工作。

Choreographer 过程中的动作也都是异步消息,这样可以确保 Choreographer 的顺利运转,也确保了第一时间执行 doTraversal(doTraversal → performTraversals 就是执行 view 的 layout、measure、draw),这个过程中如果有其他同步消息,也无法得到处理,都要等到 doTraversal 之后。

因为主线程中如果有太多消息要执行,而这些消息又是根据时间戳进行排序,如果不加一个同步屏障的话,那么遍历绘制 View 树的工作就可能被迫延迟执行,因为它也需要排队,那么就有可能出现当一帧都快结束的时候才开始计算屏幕数据,那即使这次的计算少于 16.6ms,也同样会造成丢帧现象。

那么,有了同步屏障消息的控制就能保证每次一接收到屏幕刷新信号就第一时间处理遍历绘制 View 树的工作么?

只能说,同步屏障是尽可能去做到,但并不能保证一定可以第一时间处理。因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行。

下面是部分详细的分析:

WindowManager维护着所有的Activity的DecorView和ViewRootImpl。在前面我们讲过,WindowManagerGlobal的addView方法中中初始化了ViewRootImpl,然后调用它的setView方法,将DecorView作为参数传递了进去。所以我们看看ViewRootImpl做了什么

//ViewRootImpl.java
//view是DecorView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
···
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout(); //发起布局请求
···
view.assignParent(this); //将当前ViewRootImpl对象this作为参数调用了DecorView的 assignParent
···
}
}
}

在setView()方法里调用了DecorView的assignParent

//View.java
/*

  • Caller is responsible for calling requestLayout if necessary.
  • (This allows addViewInLayout to not request a new layout.)
    */
    @UnsupportedAppUsage
    void assignParent(ViewParent parent) {
    if (mParent == null) {
    mParent = parent;
    } else if (parent == null) {
    mParent = null;
    } else {
    throw new RuntimeException(“view " + this + " being added, but”
  • " it already has a parent");
    }
    }

参数是ViewParent,而ViewRootImpl是实现了ViewParent接口的,所以在这里就将DecorView和ViewRootImpl绑定起来了。每个Activity的根布局都是DecorView,而DecorView的parent又是ViewRootImpl,所以在子View里执行invalidate()之类的工作,循环找parent,最后都会找到ViewRootImpl里来。所以实际上View的刷新都是由ViewRootImpl来控制的。

即使是界面上一个小小的 View 发起了重绘请求时,都要层层走到 ViewRootImpl,由它来发起重绘请求,然后再由它来开始遍历 View 树,一直遍历到这个需要重绘的 View 再调用它的 onDraw() 方法进行绘制。

View.invalidate()请求重绘的操作最后调用到的是ViewRootImpl.scheduleTraversals(),而ViewRootImpl.setView()方法中调用了requestLayout方法

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

最终也调用到了scheduleTraversals()方法,其实这个方法是屏幕刷新的关键。

其实打开一个 Activity,当它的 onCreate—onResume 生命周期都走完后,才将它的 DecoView 与新建的一个 ViewRootImpl 对象绑定起来,同时开始安排一次遍历 View 任务也就是绘制 View 树的操作等待执行,然后将 DecoView 的 parent 设置成 ViewRootImpl 对象。所以我们在onCreate~onResume中获取不到View宽高,界面的绘制也是在onResume之后才开始执行的。可以参考我前面的文章分析。

ViewRootImpl.scheduleTraversals()的一系列分析以及屏幕刷新机制可以参考这篇文章,这里的内容也是大部分参考它的,同步屏障相关的分析内容也在里面。

Choreographer主要作用是协调动画,输入和绘制的时间,它从显示子系统接收定时脉冲(例如垂直同步),然后安排渲染下一个frame的一部分工作。

可通过Choreographer.getInstance().postFrameCallback()来监听帧率情况;

public class FPSFrameCallback implements Choreographer.FrameCallback {
private static final String TAG = “FPS_TEST”;
private long mLastFrameTimeNanos;
private long mFrameIntervalNanos;

public FPSFrameCallback(long lastFrameTimeNanos) {
mLastFrameTimeNanos = lastFrameTimeNanos;
//每一帧渲染时间 多少纳秒
mFrameIntervalNanos = (long) (1000000000 / 60.0);
}

@Override
public void doFrame(long frameTimeNanos) { //Vsync信号到来的时间frameTimeNanos
//初始化时间
if (mLastFrameTimeNanos == 0) {
//上一帧的渲染时间
mLastFrameTimeNanos = frameTimeNanos;
}
final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames > 5) {
Log.d(TAG, "Skipped " + skippedFrames + " frames! "

  • “The application may be doing too much work on its main thread.”);
    }
    }
    mLastFrameTimeNanos = frameTimeNanos;
    //注册下一帧回调
    Choreographer.getInstance().postFrameCallback(this);
    }
    }

调用方式在Application中注册

Choreographer.getInstance().postFrameCallback(FPSFrameCallback(System.nanoTime()))

丢帧的原因:造成丢帧大体上有两类原因,一是遍历绘制 View 树计算屏幕数据的时间超过了 16.6ms;二是,主线程一直在处理其他耗时的消息,导致遍历绘制 View 树的工作迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机。

Handler锁相关问题

既然可以存在多个Handler往MessageQueue中添加数据(发送消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?

Handler.sendXXXHandler.postXXX最终会会调到MessageQueue的enqueueMessage方法

源码如下:

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.");
}
//加锁保证安全
synchronized (this) {
···
}
}

其内部通过synchronized关键字保证线程安全。同时messagequeue.next()内部也会通过synchronized加锁,确保取的时候线程安全,同时插入也会加锁。这个问题其实不难,只是看你有没有了解源码。

Handler中的同步方法

如何让handler.post消息执行之后然后再继续往下执行,同步方法runWithScissors

public final boolean runWithScissors(@NonNull Runnable r, long timeout) {
if (r == null) {
throw new IllegalArgumentException(“runnable must not be null”);
}
if (timeout < 0) {
throw new IllegalArgumentException(“timeout must be non-negative”);
}

if (Looper.myLooper() == mLooper) {
r.run();
return true;
}

BlockingRunnable br = new BlockingRunnable®;
return br.postAndWait(this, timeout);
}

Handler在系统以及第三方框架的一些应用
HandlerThread

HandlerThread继承于Thread,顾名思义,实际上是Handler和Thread的一个封装,已经为我们封装的很好很安全了,内部也通过synchronized来保证线程安全,比如getLooper方法

public Looper getLooper() {
if (!isAlive()) {
return null;
}

// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}

在线程的run方法里,所以当线程启动之后才能创建Looper并赋值给mLooper,这里的阻塞就是为了等待Looper的创建成功。同时该方法是用Public修饰的,说明该方法是提供外部调用的,Looper创建成功提供给外部使用。

IntentService

简单看一下源码就能看到Handler的应用,Handler的handMessage最终会回调到onHandleIntent方法。

public abstract class IntentService extends Service {
private volatile Looper mServiceLooper;
@UnsupportedAppUsage
private volatile ServiceHandler mServiceHandler;

如何打造一个不崩溃的程序

打造一个不崩溃的程序,可以参考我的这篇文章

Glide中的应用

Glide 相信大应该非常熟悉了,我们都知道Glide生命周期的控制(如果不了解,可以看下Glide相关文章的分析,跟LiveData 是同一个原理)是通过添加一个空的FragmentActivity 或者Fragment中,然后通过FragmentMannager管理Fragment的生命周期,从而达到生命周期的控制。下面是节选了Glide一段添加Fragment的代码:

private RequestManagerFragment getRequestManagerFragment(
@NonNull final android.app.FragmentManager fm,
@Nullable android.app.Fragment parentHint,
boolean isParentVisible) {
//1.通过FragmentManager获取 RequestManagerFragment,如果已添加到FragmentManager则返回实例,否则为空
RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
//2.如果fm里面没有则从map缓存里取
current = pendingRequestManagerFragments.get(fm);
if (current == null) {
//3.第1和2步都没有,说明确实没创建,则走创建的流程
current = new RequestManagerFragment();
current.setParentFragmentHint(parentHint);
if (isParentVisible) {
current.getGlideLifecycle().onStart();
}
//4.将新创建的fragment 保存到map容器
pendingRequestManagerFragments.put(fm, current);
//5.发送添加fragment事务事件
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
//6.发送remove 本地缓存事件
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}

//跟上面那个方法唯一的区别是 这个是Fragment 的FragmentManager,上面的是Acitivity 的FragmentManager
private SupportRequestManagerFragment getSupportRequestManagerFragment(
@NonNull final FragmentManager fm, @Nullable Fragment parentHint, boolean isParentVisible) {
SupportRequestManagerFragment current =
(SupportRequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
current = pendingSupportRequestManagerFragments.get(fm);
if (current == null) {
current = new SupportRequestManagerFragment();
current.setParentFragmentHint(parentHint);
if (isParentVisible) {
current.getGlideLifecycle().onStart();
}
pendingSupportRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_SUPPORT_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}

@Override
public boolean handleMessage(Message message) {
boolean handled = true;
Object removed = null;
Object key = null;
switch (message.what) {
case ID_REMOVE_FRAGMENT_MANAGER:
//7.移除缓存
android.app.FragmentManager fm = (android.app.FragmentManager) message.obj;
key = fm;
removed = pendingRequestManagerFragments.remove(fm);
break;
//省略代码…
}
//省略代码…
return handled;
}

看了上面的代码,大家可能会有疑惑。

  • Fragment添加到FragmentManager为什么还要保存到map容器里(第4步)?
  • 判断Fragment是否已添加为啥还要从map容器判断(第2步),FragmentManager 去find 不就可以做到了吗?

其实答案很简单,学了Handler原理之后我们知道:就是在第5步执行完之后并没有将Fragment添加到FragmentManager(事件排队中),而是发送添加Fragment的事件。接下来我们看代码

//FragmentManagerImpl.java
void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
updateOnBackPressedCallbackEnabled();
}
}
}

添加Fragment 最终会走到FragmentManagerImplscheduleCommit方法,我们可以看到他是通过Handler 发送事件。

这也就解释了为什么第5步执行完之后Fragment为什么没有立即添加到FragmentManager,所以需要Map缓存Fragment来标记是否有Fragment添加。再接着有了第6步发送移除Map缓存的消息,因为Handler处理消息是有序的。

总结

本文其实对源码的分析并没有非常仔细,却从整个系统各个方面的运用进行的不同的扩展,以及一些第三方框架中的使用,希望本文对你有帮助,喜欢的点个赞吧~

面试复习笔记:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
给文章留个小赞,就可以免费领取啦~

戳我领取:Android对线暴打面试指南超硬核Android面试知识笔记3000页Android开发者架构师核心知识笔记

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

资料已经上传在我的GitHub,或者关注后简信我【666】即可领取(无偿)。

参考文章:

zhuanlan.51cto.com/art/202007/…

blog.csdn.net/meegomeego/…

juejin.im/post/684490…

www.jianshu.com/p/dfd940e7f…

time.geekbang.org/column/arti…

xiaozhuanlan.com/topic/65243…

juejin.im/post/686972…

学习宝典

对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

学习宝典

对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-WiXTkAOl-1715452644918)]

【算法合集】

[外链图片转存中…(img-2aqwLyrR-1715452644919)]

【延伸Android必备知识点】

[外链图片转存中…(img-YWHDCuPl-1715452644919)]

【Android部分高级架构视频学习资源】
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 45
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值