写在最后
今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
这是要打脸吗?但如果在子线程中加句线程休眠模拟耗时操作的话:
程序就崩溃了,报错如下:
前面说了 Android的UI更新被设计成单线程,这里妥妥滴会报错,但却发生在延迟执行后?限于篇幅,这里就不去跟源码了,直接说原因:
ViewRootImp 在 onCreate() 时还没创建;
在 onResume()时,即ActivityThread 的 handleResumeActivity() 执行后才创建;
调用 requestLayout(),走到 checkThread() 时就报错了。
可以打个日志简单的验证下:
加上休眠:
行吧,以后去面试别人问「子线程是不是一定不可以更新UI」别傻乎乎地点头说是了。
4.引生的另一个问题
说到「只能在主线程中更新UI」我又想到另一个问题「不能在主线程中进行网络操作」
上述代码运行直接闪退,日志如下:
NetworkOnMainThreadException:网络请求在主线程进行异常。
em… 真的不能在主线程中做网络操作吗?
在 onCreate() 的 setContentView() 后插入下面两句代码:
运行下看看:
这…又打脸?先说下 StrictMode(严苟模式)
Android 2.3 引入,用于检测两大问题:ThreadPolicy(线程策略) 和 VmPolicy(VM策略)
相关方法如下:
把严苟模式的网络检测关了,就可以 在主线程中执行网络操作了,不过一般是不建议这样做的:
在主线程中进行耗时操作,可能会导致程序无响应,即 ANR (Application Not Responding)。
至于常见的ANR时间,可以在对应的源码中找到:
// ActiveServices.java → Service服务static final int SERVICE_TIMEOUT = 201000; // 前台static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; // 后台// ActivityManagerService.java → Broadcast广播、InputDispatching、ContentProviderstatic final int BROADCAST_FG_TIMEOUT = 101000; // 前台static final int BROADCAST_BG_TIMEOUT = 601000; // 后台static final int KEY_DISPATCHING_TIMEOUT = 51000; // 关键调度static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000; // 内容提供者
时间统计区间:
- 起点:System_Server 进程调用 startProcessLocked 后调用 AMS.attachApplicationLocked()
- 终点:Provider 进程 installProvider及publishContentProviders 调用到 AMS.publishContentProviders()
- 超过这个时间,系统就会杀掉 Provider 进程。
二、Handler怎么用
1.sendMessage() + handleMessage()
代码示例如下:
黄色部分会有如下警告
Handler不是静态类可能引起「内存泄露」,原因以及正确写法等下再讲。
另外,建议调用 Message.obtain() 函数来获取一个Message实例,为啥?点进源码:
从源码,可以看到obtain()的逻辑:
- 加锁,判断Message池是否为空
- ① 不为空,取一枚Message对象,正在使用标记置为0,池容量-1,返回;
- ② 为空,新建一个Message对象,返回;
Message池复用Message,可以「避免重复创建实例对象」节约内存,
另外,Message池其实是一个「单链表结构」
上述获取消息池的逻辑:
定位到下述代码,还可以知道:池的容量为50,超过
然后问题来了,Message信息什么时候加到池中?
答:当Message 被Looper分发完后,会调用 recycleUnchecked()函数,回收没有在使用的Message对象。
标志设置为FLAG_IN_USE,表示正在使用,相关属性重置,加锁,判断消息池是否满,
未满,「单链表头插法」把消息插入到表头。(获取和插入都发生在表头,像不像 栈~)
2.post(runnable)
代码示例如下:
跟下post():
实际上调用了 sendMessageDelayed() 发送消息,只不过延迟秒数为0,
那Runnable是怎么变成Message的呢?跟下getPostMessage()
噢,获取一个新的Message示例后,把 Runnable 变量的值赋值给 callback属性
3.附:其他两个种在子线程中更新UI的方法
activity.runOnUiThread()
view.post() 与 view.postDelay()
三、Handler底层原理解析
===============
终于来到稍微有点技术含量的环节,在观摩源码了解原理前,先说下几个涉及到的类。
1.涉及到的几个类
2.前戏
在我们使用Handler前,Android系统已为我们做了一系列的工作,其中就包括了
创建「Looper」和「MessageQueue」对象
上图中有写:ActivityThread 的 main函数是APP进程的入口,定位到 ActivityThread → main函数
定位到:Looper → prepareMainLooper函数
定位到:Looper → prepare函数
定位到:Looper → Looper构造函数
另外这里的 mQuitAllowed 变量,直译「退出允许」,具体作用是?跟下 MessageQueue:
em…用来 防止开发者手动终止消息队列,停止Looper循环。
3.消息队列的运行
前戏过后,创建了Looper与MessageQueue对象,接着调用Looper.loop()开启轮询。
定位到:Looper → loop函数
接着有几个问题,先是这个 myLooper() 函数:
这里的 ThreadLocal → 线程局部变量 → JDK提供的用于解决线程安全的工具类。
作用:为每个线程提供一个独立的变量副本 → 以解决并发访问的冲突问题。
本质:
每个Thread内部都维护了一个ThreadLocalMap,这个map的key是ThreadLocal,
value是set的那个值。get的时候,都是从自己的变量中取值,所以不存在线程安全问题。
主线程和子线程的Looper对象实例相互隔离的!!!
意味着:主线程和子线程Looper不是同一个!!!
知道这个以后,有个问题就解惑了:
「为什么子线程中不能直接 new Handler(),而主线程可以?」
答:主线程与子线程不共享同一个Looper实例,主线程的Looper在启动时就通过
prepareMainLooper() 完成了初始化,而子线程还需要调用 Looper.prepare()
和 Looper.loop()开启轮询,否则会报错,不信,可以试试:
直接就奔溃了~
加上试试?
可以,程序正常运行,没有报错。
对了,既然说Handler用于子线程和主线程通信,试试在主线程中给子线程的Handler发送信息,修改一波代码:
运行,直接报错:
原因:多线程并发的问题,当主线程执行到sendEnptyMessage时,子线程的Handler还没有创建。
一个简单的解决方法是:主线程延时给子线程发消息,修改后的代码示例如下:
运行结果如下:
可以,不过其实Android已经给我们封装好了一个轻量级的异步类「HandlerThread」
4.HandlerThread
HandlerThread = 继承Thread + 封装Looper
使用方法很简单,改造下我们上面的代码:
用法挺简单的,源码其实也很简单,跟一跟:
剩下一个quit()和quitSafely()停止线程,就不用说了,所以HandlerThread的核心原理就是:
- 继承Thread,getLooper()加锁死循环wait()堵塞线程;
- run()加锁等待Looper对象创建成功,notifyAll()唤醒线程;
- 唤醒后,getLooper返回由run()中生成的Looper对象;
是吧,HandlerThread的实现原理竟简单如斯,另外,顺带提个醒!!!
Java中所有类的父类是 Object 类,里面提供了wait、notify、notifyAll三个方法;
Kotlin 中所有类的父类是 Any 类,里面可没有上述三个方法!!!
所以你不能在kotlin类中直接调用,但你可以创建一个java.lang.Object的实例作为lock,
去调用相关的方法。
代码示例如下:
private val lock = java.lang.Object()
fun produce() = synchronized(lock) {
while(items>=maxItems) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items++
println(“Produced, count is i t e m s : items: items:{Thread.currentThread()}”)
lock.notifyAll()}fun consume() = synchronized(lock) {
while(items<=0) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items–
println(“Consumed, count is i t e m s : items: items:{Thread.currentThread()}”)
lock.notifyAll()
}
5.当我们用Handler发送一个消息发生了什么?
扯得有点远了,拉回来,刚讲到 ActivityThread 在 main函数中调用 Looper.prepareMainLooper
完成主线程 Looper初始化,然后调用 Looper.loop() 开启消息循环 等待接收消息。
嗯,接着说下 发送消息,上面说了,Handler可以通过sendMessage()和 post() 发送消息,
上面也说了,源码中,这两个最后调用的其实都是 sendMessageDelayed()完成的:
第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」,跟 sendMessageAtTime()
获取当前线程Looper中的MessageQueue队列,判空,空打印异常,否则返回 enqueueMessage(),跟:
这里的 mAsynchronous 是 异步消息的标志,如果Handler构造方法不传入这个参数,默认false:
这里涉及到了一个「同步屏障」的东西,等等再讲,跟:MessageQueue -> enqueueMessage
如果你了解数据结构中的单链表的话,这些都很简单。
6.Looper是怎么拣队列的消息的?
MessageQueue里有Message了,接着就该由Looper来分拣了,定位到:Looper → loop函数
// Looper.loop()
final Looper me = myLooper(); // 获得当前线程的Looper实例
final MessageQueue queue = me.mQueue; // 获取消息队列
for (;😉 { // 死循环
Message msg = queue.next(); // 取出队列中的消息
msg.target.dispatchMessage(msg); // 将消息分发给Handler
}
queue.next() 从队列拿出消息,定位到:MessageQueue -> next函数:
这里的关键其实就是:nextPollTimeoutMillis,决定了堵塞与否,以及堵塞的时间,三种情况:
等于0时,不堵塞,立即返回,Looper第一次处理消息,有一个消息处理完 ;
大于0时,最长堵塞等待时间,期间有新消息进来,可能会了立即返回(立即执行);
等于-1时,无消息时,会一直堵塞;
Tips:此处用到了Linux的pipe/epoll机制:没有消息时阻塞线程并进入休眠释放cpu资源,有消息时唤醒线程;
7.分发给Handler的消息是怎么处理的?
通过MessageQueue的queue.next()拣出消息后,调用msg.target.dispatchMessage(msg)
把消息分发给对应的Handler,跟到:Handler -> dispatchMessage
到此,关于Handler的基本原理也说的七七八八了~
8.IdleHandler是什么?
评论区有小伙子说:把idleHandler加上就完整了,那就安排下吧~
在 MessageQueue 类中有一个 static 的接口 IdleHanlder
翻译下注释:当线程将要进入堵塞,以等待更多消息时,会回调这个接口;
简单点说:当MessageQueue中无可处理的Message时回调;
作用:UI线程处理完所有View事务后,回调一些额外的操作,且不会堵塞主进程;
接口中只有一个 queueIdle() 函数,线程进入堵塞时执行的额外操作可以写这里,
返回值是true的话,执行完此方法后还会保留这个IdleHandler,否则删除。
使用方法也很简单,代码示例如下:
输出结果如下:
看下源码,了解下具体的原理:MessageQueue,定义了一个IdleHandler的列表和数组
定义了添加和删除IdleHandler的函数:
在 next() 函数中用到了 mIdleHandlers 列表:
尾声
评论里面有些同学有疑问关于如何学习material design控件,我的建议是去GitHub搜,有很多同行给的例子,这些栗子足够入门。
有朋友说要是动真格的话,需要NDK以及JVM等的知识,首现**NDK并不是神秘的东西,**你跟着官方的步骤走一遍就知道什么回事了,无非就是一些代码格式以及原生/JAVA内存交互,进阶一点的有原生/JAVA线程交互,线程交互确实有点蛋疼,但平常避免用就好了,再说对于初学者来说关心NDK干嘛,据鄙人以前的经历,只在音视频通信和一个嵌入式信号处理(离线)的两个项目中用过,嵌入式信号处理是JAVA->NDK->.SO->MATLAB这样调用的我原来MATLAB的代码,其他的大多就用在游戏上了吧,一般的互联网公司会有人给你公司的SO包的。
至于JVM,该掌握的那部分,相信我,你会掌握的,不该你掌握的,有那些专门研究JVM的人来做,不如省省心有空看看计算机系统,编译原理。
一句话,平常多写多练,这是最基本的程序员的素质,尽量挤时间,读理论基础书籍,JVM不是未来30年唯一的虚拟机,JAVA也不一定再风靡未来30年工业界,其他的系统和语言也会雨后春笋冒出来,但你理论扎实会让你很快理解学会一个语言或者框架,你平常写的多会让你很快熟练的将新学的东西应用到实际中。
初学者,一句话,多练。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
些同学有疑问关于如何学习material design控件,我的建议是去GitHub搜,有很多同行给的例子,这些栗子足够入门。
有朋友说要是动真格的话,需要NDK以及JVM等的知识,首现**NDK并不是神秘的东西,**你跟着官方的步骤走一遍就知道什么回事了,无非就是一些代码格式以及原生/JAVA内存交互,进阶一点的有原生/JAVA线程交互,线程交互确实有点蛋疼,但平常避免用就好了,再说对于初学者来说关心NDK干嘛,据鄙人以前的经历,只在音视频通信和一个嵌入式信号处理(离线)的两个项目中用过,嵌入式信号处理是JAVA->NDK->.SO->MATLAB这样调用的我原来MATLAB的代码,其他的大多就用在游戏上了吧,一般的互联网公司会有人给你公司的SO包的。
至于JVM,该掌握的那部分,相信我,你会掌握的,不该你掌握的,有那些专门研究JVM的人来做,不如省省心有空看看计算机系统,编译原理。
一句话,平常多写多练,这是最基本的程序员的素质,尽量挤时间,读理论基础书籍,JVM不是未来30年唯一的虚拟机,JAVA也不一定再风靡未来30年工业界,其他的系统和语言也会雨后春笋冒出来,但你理论扎实会让你很快理解学会一个语言或者框架,你平常写的多会让你很快熟练的将新学的东西应用到实际中。
初学者,一句话,多练。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!