···
final MessageQueue queue = me.mQueue;
···
for (;😉 { //死循环
//获取消息
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
···
msg.target.dispatchMessage(msg);
···
//回收复用
msg.recycleUnchecked();
}
}
在loop方法中是一个死循环,在这里从消息队列中不断的获取消息queue.next(),然后通过Handler(msg.target)进行消息的分发,其实并没有什么具体的绑定,因为Handler在每个线程中对应只有一个Looper和消息队列MessageQueue,自然要靠它来处理,也就是是调用Looper.loop()方法。在Looper.loop()的死循环中不断的取消息,最后回收复用。
这里要强调一下Message中的参数target(Handler),正是这个变量,每个Message才能找到对应的Handler进行消息分发,让多个Handler同时工作。
再来看看子线程中是如何处理的,首先在子线程中创建一个Handler并发送Runnable。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_three);
new Thread(new Runnable() {
@Override
public void run() {
new Handler().post(new Runnable() {
@Override
public void run() {
Toast.makeText(HandlerActivity.this,“toast”,Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
运行后可以看到错误日志,可以看到提示我们需要在子线程中调用Looper.prepare()方法,实际上就是要创建一个Looper和你的Handler进行“关联”。
--------- beginning of crash
2020-11-09 15:51:03.938 21122-21181/com.jackie.testdialog E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.jackie.testdialog, PID: 21122
java.lang.RuntimeException: Can’t create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
at android.os.Handler.(Handler.java:207)
at android.os.Handler.(Handler.java:119)
at com.jackie.testdialog.HandlerActivity$1.run(HandlerActivity.java:31)
at java.lang.Thread.run(Thread.java:919)
添加Looper.prepare()创建Looper,同时调用Looper.loop()方法开始处理消息。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_three);
new Thread(new Runnable() {
@Override
public void run() {
//创建Looper,MessageQueue
Looper.prepare();
new Handler().post(new Runnable() {
@Override
public void run() {
Toast.makeText(HandlerActivity.this,“toast”,Toast.LENGTH_LONG).show();
}
});
//开始处理消息
Looper.loop();
}
}).start();
}
这里需要注意在所有事情处理完成后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于循环等待的状态,因此不需要的时候终止Looper,调用Looper.myLooper().quit()。
看完上面的代码可能你会有一个疑问,在子线程中更新UI(进行Toast)不会有问题吗,我们Android不是不允许在子线程更新UI吗,实际上并不是这样的,在ViewRootImpl中的checkThread方法会校验mThread != Thread.currentThread(),mThread的初始化是在ViewRootImpl的的构造器中,也就是说一个创建ViewRootImpl线程必须和调用checkThread所在的线程一致,UI的更新并非只能在主线程才能进行。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
“Only the original thread that created a view hierarchy can touch its views.”);
}
}
这里需要引入一些概念,Window是Android中的窗口,每个Activity和Dialog,Toast分别对应一个具体的Window,Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,因此,它是以View的形式存在的。我们来看一下Toast中的ViewRootImpl的创建过程,调用toast的show方法最终会调用到其handleShow方法。
//Toast.java
public void handleShow(IBinder windowToken) {
···
if (mView != mNextView) {
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams); //进行ViewRootImpl的创建
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
这个mWM(WindowManager)的最终实现者是WindowManagerGlobal,其的addView方法中会创建ViewRootImpl,然后进行root.setView(view, wparams, panelParentView),通过ViewRootImpl来更新界面并完成Window的添加过程。
//WindowManagerGlobal.java
root = new ViewRootImpl(view.getContext(), display); //创建ViewRootImpl
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控件的访问加上锁机制呢?缺点有两个:
-
首先加上锁机制会让UI访问的逻辑变得复杂
-
锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
所以最简单且高效的方法就是采用单线程模型来处理UI操作。(安卓开发艺术探索)
子线程如何通知主线程更新UI(都是通过Handle发送消息到主线程操作UI的)
-
主线程中定义 Handler,子线程通过 mHandler 发送消息,主线程 Handler 的 handleMessage 更新UI。
-
用 Activity 对象的 runOnUiThread 方法。
-
创建 Handler,传入 getMainLooper。
-
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.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$MethodAndArgsCaller.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();
}
2.如果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;
}
};
3.最后才调用到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.sendXXX,handler.postXXX等方法发送消息,然后通过Looper.loop()在消息队列中不断的循环检索消息,最后交给handle.dispatchMessage方法进行消息的分发处理。
如何处理Handler使用不当造成的内存泄漏?
-
有延时消息,在界面关闭后及时移除Message/Runnable,调用handler.removeCallbacksAndMessages(null)
-
内部类导致的内存泄漏改为静态内部类,并对上下文或者Activity/Fragment使用弱引用。
同时还有一个很关键的点,如果有个延时消息,当界面关闭时,该Handler中的消息还没有处理完毕,那么最终这个消息是怎么处理的?经过测试,比如我打开界面后延迟10s发送消息,关闭界面,最终在Handler(匿名内部类创建的)的handMessage方法中还是会收到消息(打印日志)。因为会有一条MessageQueue -> Message -> Handler -> Activity的引用链,所以Handler不会被销毁,Activity也不会被销毁。
正确创建Message实例
-
通过 Message 的静态方法 Message.obtain() 获取;
-
通过 Handler 的公有方法 handler.obtainMessage()
所有的消息会被回收,放入sPool中,使用享元设计模式。
面试复习路线
多余的话就不讲了,接下来将分享面试的一个复习路线,如果你也在准备面试但是不知道怎么高效复习,可以参考一下我的复习路线,有任何问题也欢迎一起互相交流,加油吧!
这里给大家提供一个方向,进行体系化的学习:
1、看视频进行系统学习
前几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。
另外,我自己也珍藏了好几套视频,有需要的我也可以分享给你。
2、进行系统梳理知识,提升储备
客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
推荐学习资料
-
Android进阶学习全套手册
-
Android对标阿里P7学习视频
-
BAT TMD大厂Android高频面试题
不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-btHzmB8d-1712002731472)]
[外链图片转存中…(img-87OLXoOx-1712002731472)]
[外链图片转存中…(img-zqPkxNsC-1712002731473)]
[外链图片转存中…(img-hOF91rIu-1712002731473)]
[外链图片转存中…(img-b2QoQqxY-1712002731474)]
[外链图片转存中…(img-rEFkN6vc-1712002731474)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-lXjsJXw5-1712002731475)]
推荐学习资料
-
Android进阶学习全套手册
[外链图片转存中…(img-yCsJiWYC-1712002731475)]
-
Android对标阿里P7学习视频
[外链图片转存中…(img-Bxd5hbNg-1712002731475)]
-
BAT TMD大厂Android高频面试题
[外链图片转存中…(img-HgeqXaYT-1712002731475)]