Android 进阶学习(十九) 面试一周总结(一)
1 关于Handler
Handler 在面试过程中经常被问到,面试的重点也有几个,分别是
1.ThreadLocal
线程数据隔离保存,当前线程只能访问自己的存储区域,
2.Looper是一个死循环,但是为什么不会ANR
在MessageQueue 的next 方法中 执行了nativePollOnce ,这个方法是一个native方法,看不到里面的内容,但是我们可以根据和他配合的另一个方法来猜测nativeWake ,这个方法字面意思就是唤醒,可以猜测上一个方法应该是休眠
3. MessageQueue 消息队列中的消息分类
我们将消息队列中的消息分为3类,1是普通的消息,同步消息 2,异步消息,异步消息的方法对外是不公开的,3,消息屏障 消息屏障类型的消息没有target ,也就是这个消息不是用Handler 发送出来的,而是通过一个私有方法 postSyncBarrier 发送的,
经常有面试问到为什么android 只有主线程可以更新UI ,子线程为什么不能更新UI,
1:在ViewRootImpl中会保证同一帧只执行一次刷新其他的都会被过滤掉,多个线程同时操作没有什么意义,
2:既然多线程都可以就必须做线程同步,就必须用到锁的机制,这么做会降低刷新屏幕时代码的运行的速度,间接的导致刷新时卡顿的存在
3:从消息屏障的工作原理来分析一下,ViewRootImpl 在 scheduleTraversals 方法开始刷新屏幕的时候会发送一个消息屏障,这个消息屏障可以拦截MessageQueue中的同步消息,也就是普通消息,来加速对这个刷新屏幕事件的处理,只要16毫秒这个刷新屏幕的消息一出来,那么所有的MessageQueue 的消息都必须要停止获取消息,这是一个非常可怕的事情,要是不停止就是导致其他事件抢占CPU ,导致刷新卡顿,同样影响体验,
4.MessageQueue 中的next 方法的逻辑,我们将这个过程分为2步,
1.获取消息先来看一下源码
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;//
Message msg = mMessages;//获取消息
if (msg != null && msg.target == null) {//消息不为空,并且是屏障消息
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());//向后查找,知道msg不为null或者找到异步消息为止,
}
//这里再次判断消息是否为空,一个是这个消息是普通消息,再一个如果是消息屏障,会向后查找异步消息,此时可能为空
if (msg != null) {
if (now < msg.when) {//如果是延迟消息,计算延迟时间
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
第一步获取消息就完成了,
2. IdleHandler 消息机制
如果当前没有事情可做,MessageQueue 就会查找自己的mIdleHandlers, 如果返回false,那么这个IdleHandler 只会执行一次,这个就是消息队列中的空闲机制,并不是很稳定,但是发现在LeakCanary当中被应用的非常好,因为是否要开始检测他其实就是一个辅助手段,并不是必须要的,将这个消息放在空闲消息中的设计非常好,
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
2.volatile 关键字
其实在实际开发中的应用不多,大多数应用在DCL单利模式中, 众所周知的是volatile + synchronized + 双重检索 可以非常有效的保证单利模式只有一个实例,那么在这个里面volatile 的作用是什么呢, volatile 的可见性又是什么意思呢, 在 美团 小米 的面试过程中都被闻到过,所以着重的看了一下, android 的主缓存是硬盘缓存,他的保存数据和获取数据的速度是远远小于cpu的执行速度的,那么为了加速cpu的运行速度,每一个cpu都会在他的上面增加一个cpu临时缓存,cpu将数据计算后并不是马上更新主缓存,而是先跟新自己的缓存,使用volatile 关键字表示的字段,会在cpu计算后马上将这个数据同步到主缓存,保证数据的有效性,同时其他cpu所获取的这个对象将会失效,下次使用的时候就必须先从主缓存中获取,这样就可以保证数据的一致性,同时还可以防止指令重排 ,在单利模式中主要的作用就是防止指令重排,创建一个对象的过程分别是 1.加载类 2.指定内存空间 3.创建对象 4.创建引用指针 其中3和4 可能存在指令重排,
3. synchronized 关键字
这个关键字在日常开发中经常会遇到,大家都知道他是同步锁,那么类锁和对象锁有什么区别吗,synchronized 的实现逻辑是什么呢,在美团的面试过程中给我问的一愣一愣的,
类锁可以应用在静态方法中,我们知道synchronized 锁住的是一个关于锁的对象,类锁的对象只有一个,而对象可以通过类创建,可以有多个,
再来说一下synchronized 的实现逻辑,这个必须通过编译java 文件才可以,
image.png
从网上找了一个图片,这个图片就是synchronized 编译后的结果,可以看到monitor 就是关键逻辑,只要明白了他就知道他是怎么实现的了,
关于这种问题是在是不了解 ,大家可以搜一下 java monitor 自己去百度一下
4.ThreadPoolExecutor execute 方法的实现逻辑
关于这个问题,在小米的二面面试过程中确实是问到过,而这个自己在复习的过程中确实也确实有看过,
1.如果主要线程没满,那么就会开启线程去执行这个任务,
2.如果主要线程已经满了,则会添加到任务队列中,如果成功则等待执行,如果失败则再次判断主要线程是否有执行完的,如果有则开启线程执行,
3.如果队列添加失败,那么则会判断最大线程数是否超限,如果没有则开启线程执行,如果超限了,则抛异常
在这个执行过程中,会发现并不是先添加的任务就会先执行,当任务缓存队列满了后,添加的任务可能就会先执行,
5.ANR 产生的原因
其实anr 这个问题是非常难以定位,导致anr的地方也非常多,但是现在开发者应该都会规避一些阻塞的方法,但是为什么还是会导致anr呢,其中一类是不可避免的,那就是在操作触摸事件时,如果对数据计算或者操作处理不及时,就会导致anr,这个就必须要优化算法,调整展示结构,其次就是在明明不耗时的方法内导致anr,那是为什么呢, 其实我们可以大胆猜测,是什么会导致现在的方法停止执行,没错就是gc ,在gc执行过程中就会让其他地方停止执行,因为gc在执行过程中会使用标记整理,标记复制等方法,先将数据标记一下,如果其他地方还在执行,将会对标记产生影响,所以在执行gc过程其他地方都是停止的,从这里我们就从anr 转换到了内存问题上,这里我们可以操作的空间就比较多了,泄露 抖动 图片的压缩 和 inbitmap ,这里给我们的启发就是想要分析anr,同时还要分析一些其他的信息,比如内存,只通过anr 说明不了什么问题