关于Handler的这些都没搞懂,还怎么去跳槽,4个改变你编程技能的小技巧

//建立Binder通道 (创建新线程)
thread.attach(false);

Looper.loop(); //消息循环运行
throw new RuntimeException(“Main thread loop unexpectedly exited”);
}

Activity的生命周期都是依靠主线程的 Looper.loop,当收到不同Message时则采用相应措施:一旦退出消息循环,那么你的程序也就可以退出了。 从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。

thread.attach(false)方法函数中便会创建一个Binder线程(具体是指ApplicationThreadBinder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过HandlerMessage发送给主线程。「Activity 启动过程」

比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;

再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。

主线程的消息又是哪来的呢?当然是App进程中的其他线程通过Handler发送给主线程进程

3.ActivityThread 的动力是什么?

进程 每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程

线程 线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片。

其实承载ActivityThread的主线程就是由Zygote fork而创建的进程。

4.Handler 是如何能够线程切换

其实看完上面我们大致也清楚线程间是共享资源的。所以Handler处理不同线程问题就只要注意异步情况即可。

这里再引申出Handler的一些小知识点。 Handler创建的时候会采用当前线程的Looper来构造消息循环系统,Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的。(敲黑板)

那么Handler内部如何获取到当前线程的Looper呢—–ThreadLocalThreadLocal可以在不同的线程中互不干扰的存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper

当然需要注意的是:

①线程是默认没有Looper的,如果需要使用Handler,就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread
ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。

系统为什么不允许在子线程中访问UI?(摘自《Android开发艺术探索》)

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

①首先加上锁机制会让UI访问的逻辑变得复杂
②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。 所以最简单且高效的方法就是采用单线程模型来处理UI操作

5.子线程有哪些更新UI的方法

主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI。 用Activity对象的runOnUiThread方法。 创建Handler,传入getMainLooperView.post(Runnabler)

runOnUiThread 第一种咱们就不分析了,我们来看看第二种比较常用的写法。

先重新温习一下上面说的

Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的。(敲黑板)

new Thread(new Runnable() {
@Override
public void run() {

runOnUiThread(new Runnable() {
@Override
public void run() {
//DO UI method

}
});

}
}).start();

final Handler mHandler = new Handler();

public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);//子线程(非UI线程)
} else {
action.run();
}
}

进入Activity类里面,可以看到如果是在子线程中,通过mHandler发送的更新UI消息。 而这个Handler是在Activity中创建的,也就是说在主线程中创建,所以便和我们在主线程中使用Handler更新UI没有差别。 因为这个Looper,就是ActivityThread中创建的Looper(Looper.prepareMainLooper())

创建Handler,传入getMainLooper 那么同理,我们在子线程中,是否也可以创建一个Handler,并获取MainLooper,从而在子线程中更新UI呢? 首先我们看到,在Looper类中有静态对象sMainLooper,并且这个sMainLooper就是在ActivityThread中创建的MainLooper

private static Looper sMainLooper; // guarded by Looper.class

public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException(“The main Looper has already been prepared.”);
}
sMainLooper = myLooper();
}
}

所以不用多说,我们就可以通过这个sMainLooper来进行更新UI操作

new Thread(new Runnable() {
@Override
public void run() {

Log.e(“qdx”, "step 1 "+Thread.currentThread().getName());

Handler handler=new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {

//Do Ui method
Log.e(“qdx”, "step 2 "+Thread.currentThread().getName());
}
});

}
}).start();

View.post(Runnabler) 老样子,我们点入源码
//View

/**

  • Causes the Runnable to be added to the message queue.

  • The runnable will be run on the user interface thread.
  • @param action The Runnable that will be executed.
  • @return Returns true if the Runnable was successfully placed in to the
  •     message queue.  Returns false on failure, usually because the
    
  •     looper processing the message queue is exiting.
    

*/
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action); //一般情况走这里
}

// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

/**

  • A Handler supplied by a view’s {@link android.view.ViewRootImpl}. This
  • handler can be used to pump events in the UI events queue.
    */
    final Handler mHandler;

居然也是Handler从中作祟,根据Handler的注释,也可以清楚该Handler可以处理UI事件,也就是说它的Looper也是主线程的sMainLooper。这就是说我们常用的更新UI都是通过Handler实现的。

另外更新UI 也可以通过AsyncTask来实现,难道这个AsyncTask的线程切换也是通过 Handler 吗? 没错,也是通过Handler……

6.子线程中Toast,showDialog,的方法
可能有些人看到这个问题,就会想: 子线程本来就不可以更新UI的啊 而且上面也说了更新UI的方法.兄台且慢,且听我把话写完

new Thread(new Runnable() {
@Override
public void run() {

Toast.makeText(MainActivity.this, “run on thread”, Toast.LENGTH_SHORT).show();//崩溃无疑

}
}).start();

看到这个崩溃日志,是否有些疑惑,因为一般如果子线程不能更新UI控件是会报如下错误的(子线程不能更新UI)

所以子线程不能更新Toast的原因就和Handler有关了,据我们了解,每一个Handler都要有对应的Looper对象,那么。 满足你。

new Thread(new Runnable() {
@Override
public void run() {

Looper.prepare();
Toast.makeText(MainActivity.this, “run on thread”, Toast.LENGTH_SHORT).show();
Looper.loop();

}
}).start();

这样便能在子线程中Toast,不是说子线程…? 老样子,我们追根到底看一下Toast内部执行方式
//Toast

/**

  • Show the view for the specified duration.
    */
    public void show() {

INotificationManager service = getService();//从SMgr中获取名为notification的服务
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
service.enqueueToast(pkg, tn, mDuration);//enqueue? 难不成和Handler的队列有关?
} catch (RemoteException e) {
// Empty
}
}

在show方法中,我们看到Toast的show方法和普通UI 控件不太一样,并且也是通过Binder进程间通讯方法执行Toast绘制。这其中的过程就不在多讨论了,有兴趣的可以在NotificationManagerService类中分析。

现在把目光放在TN 这个类上(难道越重要的类命名就越简洁,如H类),通过TN 类,可以了解到它是Binder的本地类。在Toast的show方法中,将这个TN对象传给NotificationManagerService就是为了通讯!并且我们也在TN中发现了它的show方法。

private static class TN extends ITransientNotification.Stub {//Binder服务端的具体实现类

/**

  • schedule handleShow into the right thread
    */
    @Override
    public void show(IBinder windowToken) {
    mHandler.obtainMessage(0, windowToken).sendToTarget();
    }

final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};

}

看完上面代码,就知道子线程中Toast报错的原因,因为在TN中使用Handler,所以需要创建Looper对象。 那么既然用Handler来发送消息,就可以在handleMessage中找到更新Toast的方法。 在handleMessage看到由handleShow处理。
//Toast的TN类

public void handleShow(IBinder windowToken) {

mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mWM.addView(mView, mParams);//使用WindowManager的addView方法
trySendAccessibilityEvent();
}
}

看到这里就可以总结一下:

Toast本质是通过window显示和绘制的(操作的是window),而主线程不能更新UI 是因为ViewRootImplcheckThread方法在Activity维护的View树的行为。 Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,所以为了防止在创建TN时抛出异常,需要在子线程中使用Looper.prepare();和Looper.loop();(但是不建议这么做,因为它会使线程无法执行结束,导致内存泄露)

Dialog亦是如此。同时我们又多了一个知识点要去研究:Android 中Window是什么,它内部有什么机制?

7.如何处理Handler 使用不当导致的内存泄露? 首先上文在子线程中为了节目效果,使用如下方式创建Looper

Looper.prepare();

Looper.loop();

实际上这是非常危险的一种做法

在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。(【 Looper.myLooper().quit(); 】)

那么,如果在Handler的handleMessage方法中(或者是run方法)处理消息,如果这个是一个延时消息,会一直保存在主线程的消息队列里,并且会影响系统对Activity的回收,造成内存泄露。

具体可以参考Handler内存泄漏分析及解决

总结一下,解决Handler内存泄露主要2点

1 有延时消息,要在Activity销毁的时候移除Messages
2 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。

总结

想不到Handler居然可以腾出这么多浪花,与此同时感谢前辈的摸索。

另外Handler还有许多不为人知的秘密,等待大家探索,下面我再简单的介绍两分钟

HandlerThread
IdleHandler

HandlerThread

HandlerThread继承Thread,它是一种可以使用Handler的Thread,它的实现也很简单,在run方法中也是通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环(与我们手动创建方法基本一致),这样在实际的使用中就允许在HandlerThread中创建Handler了。

由于HandlerThread的run方法是一个无限循环,因此当不需要使用的时候通过quit或者quitSafely方法来终止线程的执行。

HandlerThread的本质也是线程,所以切记关联的Handler中处理消息的handleMessage为子线程。

IdleHandler

/**

  • Callback interface for discovering when a thread is going to block
  • waiting for more messages.
    /
    public static interface IdleHandler {
    /
    *
  • Called when the message queue has run out of messages and will now
  • wait for more. Return true to keep your idle handler active, false
  • to have it removed. This may be called if there are still messages
  • pending in the queue, but they are all scheduled to be dispatched
  • after the current time.
    */
    boolean queueIdle();
    }

根据注释可以了解到,这个接口方法是在消息队列全部处理完成后或者是在阻塞的过程中等待更多的消息的时候调用的,返回值false表示只回调一次,true表示可以接收多次回调。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

结语

看到这篇文章的人不知道有多少是和我一样的Android程序员。

35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。

就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。
[外链图片转存中…(img-DBhz611a-1712062266049)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值