2024年安卓最新面试官:如何监测应用的 FPS ?,腾讯社招前端面试题

最后

感谢您的阅读,在文末给大家准备一个福利。本人从事Android开发已经有十余年,算是一名资深的移动开发架构师了吧。根据我的观察发现,对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

所以在此将我十年载,从萌新小白一步步成长为Android移动开发架构师的学习笔记,从Android四大组件到手写实现一个架构设计,我都有一一的对应笔记为你讲解。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

还记得我们跟了这么久在干嘛吗?为了探究 View 的刷新流程,我们跟着 View.invalidate() 方法一路追到 ViewGroup.invalidateChild() ,其中递归调用 parent 的 invalidateChildInParent() 方法。所以我们在 给 DecorView 找爸爸 。现在很清晰了,DecorView 的爸爸就是 ViewRootImpl ,所以最终调用的就是 ViewRootImpl.invalidateChildInParent() 方法。

ViewRootImpl.java

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
// 1. 线程检查
checkThread();

if (dirty == null) {
// 2. 调用 scheduleTraversals()
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}

// 3. 调用 scheduleTraversals()
invalidateRectOnScreen(dirty);

return null;
}

无论是注释 2 处的 invalite() 还是注释 3 处的 invalidateRectOnScreen() ,最终都会调用到 scheduleTraversals() 方法。

scheduleTraversals() 在 View 绘制流程中是个极其重要的方法,我不得不单独开一节来聊聊它。

承上启下的 “编舞者”

上一节中,我们从 View.invalidate() 方法开始追踪,一直跟到 ViewRootImpl.scheduleTraversals() 方法。

ViewRootImpl.java

void scheduleTraversals() {
// 1. 防止重复调用
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 2. 发送同步屏障,保证优先处理异步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 3. 最终会执行 mTraversalRunnable 这个任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

}
}

  1. mTraversalScheduled 是个布尔值,防止重复调用,在一次 vsync 信号期间多次调用是没有意义的
  2. 利用 Handler 的同步屏障机制,优先处理异步消息
  3. Choreographer 登场

到这里,鼎鼎大名的 编舞者 —— Choreographer [ˌkɔːriˈɑːɡrəfər] 就该出场了(为了避免面试中出现不会读单词的尴尬,掌握一下发音还是必须的)。

通过 mChoreographer 发送了一个任务 mTraversalRunnable ,最终会在某个时刻被执行。在看源码之前,先抛出来几个问题:

  1. mChoreographer 是在什么时候初始化的?
  2. mTraversalRunnable 是个什么鬼?
  3. mChoreographer 是如何发送任务以及任务是如何被调度执行的?

围绕这三个问题,我们再回到源码中。

先来看第一个问题,这就得回到上一节介绍过的 WindowManagerGlobal.addView() 方法。

WindowManagerGlobal.java

// 参数 view 就是 DecorView
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {

ViewRootImpl root;
// 1. 初始化 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);

mViews.add(view);
mRoots.add(root);

root.setView(view, wparams, panelParentView);

}

注释 1 处 新建了 ViewRootImpl 对象,跟进 ViewRootImpl 的构造函数。

ViewRootImpl.java

public ViewRootImpl(Context context, Display display) {
mContext = context;
// 1. IWindowSession 代理对象,与 WMS 进行 Binder 通信
mWindowSession = WindowManagerGlobal.getWindowSession();

mThread = Thread.currentThread();

// IWindow Binder 对象
mWindow = new W(this);

// 2. 初始化 mAttachInfo
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);

// 3. 初始化 Choreographer,通过 Threadlocal 存储
mChoreographer = Choreographer.getInstance();

}

ViewRootImpl 的构造函数中,注释 3 处初始化了 mChoreographer,调用的是 Choreographer.getInstance() 方法。

Choreographer.java

public static Choreographer getInstance() {
return sThreadInstance.get();
}

sThreadInstance 是一个 ThreadLocal<Choreographer> 对象。

Choreographer.java

private static final ThreadLocal sThreadInstance =
new ThreadLocal() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException(“The current thread must have a looper!”);
}
// 新建 Choreographer 对象
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};

所以 mChoreographer 保存在 ThreadLocal 中的线程私有对象。它的构造函数中需要传入当前线程(这里就是主线程)的 Looper 对象。

这里再插一个题外话,主线程 Looper 是在什么时候创建的? 回顾一下应用进程的创建流程:

  • 调用 Process.start() 创建应用进程

  • ZygoteProcess 负责和 Zygote 进程建立 socket 连接,并将创建进程需要的参数发送给 Zygote 的 socket 服务端

  • Zygote 服务端接收到参数之后调用 ZygoteConnection.processOneCommand() 处理参数,并 fork 进程

  • 最后通过 findStaticMain() 找到 ActivityThread 类的 main() 方法并执行,子进程就启动了

ActivityThread 并不是一个线程,但它是运行在主线程上的,主线程 Looper 就是在它的 main() 方法中执行的。

ActivityThread.java

public static void main(String[] args) {

// 创建主线程 Looper
Looper.prepareMainLooper();

// 创建 ActivityThread ,并 attach(false)
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

// 开启主线程消息循环
Looper.loop();
}

Looper 也是存储在 ThreadLocal 中的。

再回到 Choreographer,我们来看一下它的构造函数。

Choreographer.java

private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
// 处理事件
mHandler = new FrameHandler(looper);
// USE_VSYNC 在 Android 4.1 之后默认为 true,
// FrameDisplayEventReceiver 是个 vsync 事件接收器
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;

// 一帧的时间,60pfs 的话就是 16.7ms
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
// 回调队列
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}

这里出现了几个新面孔,FrameHandlerFrameDisplayEventReceiverCallbackQueue,这里暂且不表,先混个脸熟,后面会一一说到。

介绍完 Choreographer 是如何初始化的,再回到 Choreographer 发送任务那块。

mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

我们看看 mTraversalRunnable 是什么东西。

ViewRootImpl.java

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

没什么特别的,它就是一个 Runnable 对象,run() 方法中会执行 doTraversal() 方法。

ViewRootImpl.java

void doTraversal() {
if (mTraversalScheduled) {
// 1. mTraversalScheduled 置为 false
mTraversalScheduled = false;
// 2. 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

// 3. 开始布局,测量,绘制流程
performTraversals();

}

再对比一下最开始发起绘制的 scheduleTraversals() 方法:

ViewRootImpl.java

void scheduleTraversals() {
// 1. mTraversalScheduled 置为 true,防止重复调用
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 2. 发送同步屏障,保证优先处理异步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 3. 最终会执行 mTraversalRunnable 这个任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

}
}

仔细分别看一下上面两个方法的注释 1、2、 3,还是很清晰的。mTraversalRunnable 被执行后最终会调用 performTraversals() 方法,来完成整个 View 的测量,布局和绘制流程。

分析到这里,就差最后一步了,mTraversalRunnable 是如何被调度执行的? 我们再回到 Choreographer.postCallback() 方法。

Choreographer.java

public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}

public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {

postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

// 传入的参数依次是 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null,0
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {

synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
// 1. 将 mTraversalRunnable 塞入队列
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) { // 立即执行
// 2. 由于 delayMillis 是 0,所以会执行到这里
scheduleFrameLocked(now);
} else { // 延迟执行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

首先根据 callbackType(这里是 CALLBACK_TRAVERSAL) 将稍后要执行的 mTraversalRunnable 放入相应队列中,其中的具体逻辑就不看了。

然后由于 delayMillis 是 0,所以 dueTime 和 now 是相等的,所以直接执行 scheduleFrameLocked(now) 方法。如果 delayMillis 不为 0 的话,会通过 FrameHandler 发送一个延时消息,最后执行的仍然是 scheduleFrameLocked(now) 方法。

Choreographer.java

private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) { // Android 4.1 之后 USE_VSYNCUSE_VSYNC 默认为 true

// 如果是当前线程,直接申请 vsync,否则通过 handler 通信
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
// 发送异步消息
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else { // 未开启 vsync,4.1 之后默认开启
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

开头到现在,已经好几次提到了 VSYNC,官方也有一个视频介绍 Android Performance Patterns: Understanding VSYNC ,大家可以看一看。简而言之,VSYNC 是为了解决屏幕刷新率和 GPU 帧率不一致导致的 “屏幕撕裂” 问题。VSYNC 在 PC 端是很久以来就存在的技术,但在 4.1 之后,Google 才将其引入到 Android 显示系统中,以解决饱受诟病的 UI 显示不流畅问题。

再说的简单点,可以把 VSYNC 看成一个由硬件发出的定时信号,通过 Choreographer 监听这个信号。每当信号来临时,统一开始绘制工作。这就是 scheduleVsyncLocked() 方法的工作内容。

Choreographer.java

private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

mDisplayEventReceiverFrameDisplayEventReceiver 对象,但它并没有 scheduleVsync() 方法,而是直接调用的父类方法。FrameDisplayEventReceiver 的父类是 DisplayEventReceiver

DisplayEventReceiver.java

public abstract class DisplayEventReceiver {

public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "

  • “receiver has already been disposed.”);
    } else {
    // 注册监听 vsync 信号,会回调 dispatchVsync() 方法
    nativeScheduleVsync(mReceiverPtr);
    }
    }

// 有 vsync 信号时,由 native 调用此方法
private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
// timestampNanos 是 vsync 回调的时间
onVsync(timestampNanos, builtInDisplayId, frame);
}

public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
}
}

scheduleVsync() 方法中会通过 nativeScheduleVsync() 方法注册下一次 vsync 信号的监听,从方法名也能看出来,下面会进入 native 调用,水平有限,就不追进去了。

注册监听之后,当下次 vsync 信号来临时,会通过 jni 回调 java 层的 dispatchVsync() 方法,其中又调用了 onVsync() 方法。父类 DisplayEventReceiveronVsync() 方法是个空实现,我们再回到子类 FrameDisplayEventReceiver ,它是 Choreographer 的内部类。

Choreographer.java

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private long mTimestampNanos;
private int mFrame;

// vsync 信号监听回调
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

long now = System.nanoTime();
// // timestampNanos 是 vsync 回调的时间,不能比 now 大
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)

  • " ms in the future! Check that graphics HAL is generating vsync "
  • “timestamps using the correct timebase.”);
    timestampNanos = now;
    }

    mTimestampNanos = timestampNanos;
    mFrame = frame;
    // 这里传入的是 this,会回调本身的 run() 方法
    Message msg = Message.obtain(mHandler, this);
    // 这是一个异步消息,保证优先执行
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

@Override
public void run() {
doFrame(mTimestampNanos, mFrame);
}
}

onVsync() 回调中,向主线程发送了一个异步消息,注意 sendMessageAtTime() 方法参数中的时间是 timestampNanos / TimeUtilstimestampNanos 是 vsync 信号的时间戳,单位是纳秒,所以这里做一个除法转换为毫秒。代码执行到这里的时候 vsync 信号已经发生,所以 timestampNanos 是比当前时间小的。这样这个消息塞进 MessageQueue 的时候就可以直接塞到前面了。另外 callback 是 this,所以当消息被执行时,调用的是自己的 run() 方法,run() 方法中调用的是 doFrame() 方法。

Choreographer.java

void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}

long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
// 计算超时时间
// frameTimeNanos 是 vsync 信号回调的时间,startNanos 是当前时间戳
// 相减得到主线程的耗时时间
final long jitterNanos = startNanos - frameTimeNanos;
// mFrameIntervalNanos 是一帧的时间
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
// 掉帧超过 30 帧,打印 log
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "

  • “The application may be doing too much work on its main thread.”);
    }
    final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
    frameTimeNanos = startNanos - lastFrameOffset;
    }

    }

try {
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

// doCallBacks() 开始执行回调
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
}

}

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上我搜集整理的2019-2021BATJ 面试真题解析,我把大厂面试中常被问到的技术点整理成了PDF,包知识脉络 + 诸多细节。

节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?

1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

[外链图片转存中…(img-kIKtOt4R-1715833315606)]

腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

[外链图片转存中…(img-ZkQjFCML-1715833315606)]

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值