去大厂面试,结果没想到一个Handler还有中高级几种问法,我慌了---

本文探讨了Java中ThreadLocal的内存管理机制,指出其如何避免内存泄漏,并解释了在多线程环境和线程池中使用时可能导致的问题。同时,文章还介绍了epoll在AndroidHandler中的应用,以及如何通过Choreographer管理和优化UI渲染,防止由于主线程处理过多同步消息导致的丢帧问题。

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

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

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

img

img

img

img

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

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

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

image

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

image

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

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

了不少精力),包含知识脉络 + 分支细节

[外链图片转存中…(img-Z2o8sWUY-1712383883156)]

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

[外链图片转存中…(img-vkyShfss-1712383883156)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值