2024年安卓最新[Matrix系列-3]: TracePlugin 之 FrameTrace 源码分析,2024年最新大厂面试问题

最后

考虑到文章的篇幅问题,我把这些问题和答案以及我多年面试所遇到的问题和一些面试资料做成了PDF文档

喜欢的朋友可以关注、转发、点赞 感谢!

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

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

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

final synchronized public void onStartTrace() {

if (!isAlive) {

    this.isAlive = true;

    onAlive();

}

}




`onStartTrace()`方法则调用了`onAlive()`:



@Override

public void onAlive() {

super.onAlive();

if (isFPSEnable) {

    UIThreadMonitor.getMonitor().addObserver(this);

    Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);

}

}




*   说明:



如果 `isFPSEnable=true`,则注册监听 `LooperObserver` 类的回调。 至此,FrameTrace的初始化工作已经完成了。 后面就是等待回调信息过来,对数据进行处理后整合上报。所以我们来看看它父类的父类: `LooperObserver`。



[]( )1.3 LooperObserver 类

------------------------------------------------------------------------------------



public abstract class LooperObserver {

private boolean isDispatchBegin = false;



@CallSuper

public void dispatchBegin(long beginNs, long cpuBeginNs, long token) {

    isDispatchBegin = true;

}



public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {



}



@CallSuper

public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {

    isDispatchBegin = false;

}



public boolean isDispatchBegin() {

    return isDispatchBegin;

}

}




`LooperObserver` 是一个抽象类,它封装了 looper 消息处理开始和结束的回调,`dispatchBegin` 和 `dispatchEnd`这两个好理解,**但是 `doFrame` 是什么回调呢?** 我们一个一个来看吧~



### []( )1.3.1 dispatchBegin 方法解析



dispatchBegin(long beginNs, long cpuBeginNs, long token)



*   beginNs: 消息开始分发时候的时间点,单位为纳秒。通过 `System.nanoTime()` 获得。

*   cpuBeginNs: 消息开始时主线程的线程时间,单位:毫秒通过`SystemClock.currentThreadTimeMillis()`获得。

*   token: 跟 beginNs 一样。



### []( )1.3.2 dispatchEnd 方法解析



dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame)



*   beginNs: 消息开始分发时候的时间点,跟 `dispatchBegin` 中的 `beginNs` 是同一个值。

*   cpuBeginMs: 跟 `dispatchBegin` 中的 `cpuBeginMs` 是同一个值。

*   endNs: 消息结束分发时的时间点,单位:纳秒。

*   cpuEndMs: 消息结束分发时主线程时间点,单位:毫秒。

*   token: 跟 `dispatchBegin` 中的 `token` 是同一个值。

*   isVsyncFrame: 用来判断 `looper` 中消息是否是UI渲染帧。 默认为false。matrix 通过 `UIThreadMonitor` 类中 `addFrameCallback(CALLBACK_INPUT, this, true);` 开始监听input事件,在`runnable` 开始执行时候,说明此时looper中的消息是处理ui 绘制的。



@Override

public synchronized void onStart() {

if (!isInit) {

    MatrixLog.e(TAG, "[onStart] is never init.");

    return;

}

if (!isAlive) {

    this.isAlive = true;

    synchronized (this) {

        MatrixLog.i(TAG, "[onStart] callbackExist:%s %s", Arrays.toString(callbackExist), Utils.getStack());

        callbackExist = new boolean[CALLBACK_LAST + 1];

    }

    queueStatus = new int[CALLBACK_LAST + 1];

    queueCost = new long[CALLBACK_LAST + 1];

    // 

    addFrameCallback(CALLBACK_INPUT, this, true);

}

}




### []( )1.3.3 doFrame() 方法解析



首先,我们想要知道 `doFrame`方法在哪里被调用? 其实是在 `UIThreadMonitor` 类的 `dispatchEnd`方法中被调用的:



private void dispatchEnd() {

//省略…

synchronized (observers) {

for (LooperObserver observer : observers) {

    if (observer.isDispatchBegin()) {

        observer.doFrame(AppMethodBeat.getVisibleScene(), startNs, endNs, isVsyncFrame, intendedFrameTimeNs, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);

    }

}

}

//省略…

}




**方法签名含义如下:**



doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs)



*   focusedActivity: 当前顶层的activity名字

*   startNs:消息开始分发的时间点 纳秒

*   endNs:消息分发结束的时间点 纳秒

*   isVsyncFrame: 是否是UI渲染帧。只要触发了input `callback`回调,那肯定是渲染消息。

*   intendedFrameTimeNs:当前时间,纳秒。如果反射获取失败,则把`startNs`作为默认值。 通过反射`Choreographer`内部类 `FrameDisplayEventReceiver`类中的变量:`mTimestampNanos`。

*   inputCostNs:input耗时。现在这三个比较简单了吧

*   animationCostNs:animation耗时。

*   traversalCostNs:traversal耗时。



**这里先提出一个问题:**



`dispatchEnd` 是`looper` 中每个消息处理完成后都会回调的,其中一些消息肯定不是UI渲染。`那么doFrame` 肯定每次也会回调,也就是说每一次消息处理都当成了一帧渲染完毕。



那么这不是有问题吗?不会造成错误统计? 后面会给出答案~



[]( )1.4 总结

----------------------------------------------------------------------



分析了这么久,我们终于搞懂了FrameTrace背后的运行原理,主要做了以下两点:



1.  通过反射方式注册 callback到 Choreographer 类的callback链表中。计算input、animation、traversal三个阶段的耗时。

2.  往 `UIThreadMonitor` 中注册消息回调,主要关注的是每一帧消息处理的`doFrame`方法。



在拿到这些数据后,matrix是怎么去做处理和上报的呢? 我们接着往下看。



[]( )二、 FrameTrace的数据处理

==================================================================================



经过分析,我们只关心 `doFrame` 方法即可。



[]( )2.1 doFrame 方法

------------------------------------------------------------------------------



@Override

public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {

if (isForeground()) {

    notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

}

}




只有在前台才会去做数据的收集和处理,后台情况下不会处理。为什么? 因为应用不可见的时候是不会进行UI渲染的。这是系统来控制的,避免造成资源白白浪费。



接着看 notifyListener 方法:



private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,

                        final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {

long traceBegin = System.currentTimeMillis();

try {

    final long jiter = endNs - intendedFrameTimeNs;

    //整个绘制过程的帧数: 实际上一帧的耗时/理论每一帧的时间。因此,就是卡顿导致的掉帧数

    final int dropFrame = (int) (jiter / frameIntervalNs);

    if (dropFrameListener != null) {

        //触发掉帧数阈值,则分发掉帧数回调

        if (dropFrame > dropFrameListenerThreshold) {

            try {

                if (AppActiveMatrixDelegate.getTopActivityName() != null) {

                    // 这里可以先不管 。。。

                    long lastResumeTime = lastResumeTimeMap.get(AppActiveMatrixDelegate.getTopActivityName());

                    dropFrameListener.dropFrame(dropFrame, AppActiveMatrixDelegate.getTopActivityName(), lastResumeTime);

                }

            } catch (Exception e) {

                MatrixLog.e(TAG, "dropFrameListener error e:" + e.getMessage());

            }

        }

    }



    droppedSum += dropFrame;

    durationSum += Math.max(jiter, frameIntervalNs);



    synchronized (listeners) {

        for (final IDoFrameListener listener : listeners) {

            if (config.isDevEnv()) {

                listener.time = SystemClock.uptimeMillis();

            }

            if (null != listener.getExecutor()) {

                //当超过多少帧才会去收集

                // 帧数阈值>0 表示只有设置了多少帧才开始收集。如果没设置,那我没必要去分析回放。

                if (listener.getIntervalFrameReplay() > 0) {

                    //这个是开了fpsmonitor开关才会走这里,否则是默认是走else

                    listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,

                            intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

                } else {

                    //如果我收集了,那就不执行这里的逻辑

                    listener.getExecutor().execute(new Runnable() {

                        @Override

                        public void run() {

                            //做UI展示用,

                            listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,

                                    intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

                        }

                    });

                }

            } else {

                listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,

                        intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

            }



            if (config.isDevEnv()) {

                listener.time = SystemClock.uptimeMillis() - listener.time;

                MatrixLog.d(TAG, "[notifyListener] cost:%sms listener:%s", listener.time, listener);

            }

        }

    }

} finally {

    long cost = System.currentTimeMillis() - traceBegin;

    if (config.isDebug() && cost > frameIntervalNs) {

        MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", listeners.size(), cost);

    }

}

}




**说明:**



这个方法看起来比较长,主要做了两件事情:



1.  计算掉帧数

2.  通过异步的方式收集数据进行处理



### []( )2.1.1 掉帧数的计算



final long jiter = endNs - intendedFrameTimeNs;

//整个绘制过程的帧数: 实际上一帧的耗时/理论每一帧的时间。因此,就是卡顿导致的掉帧数

final int dropFrame = (int) (jiter / frameIntervalNs);




原理: 理论上我们要求一帧的渲染时间为16.66ms。那么用实际上一帧花费的时间除以理论的时间就得到了这段时间的帧数。 举个例子:



*   假设一帧实际上花费时间:<16.66ms,那么 dropFrame=0。表示没有掉帧,非常nice。

*   假设一帧实际上花费时间:=16.66ms,那么 dropFrame=1。表示没有掉帧,非常nice。

*   假设一帧实际上花费时间:16.66ms< <约32ms,那么 dropFrame=1。虽然没有达到理论值,但基本认为nice。

*   假设一帧实际上花费时间:=32ms,那么 dropFrame=2。认为掉了一帧,但基本认为nice。



按照这种计算,matrix根据掉帧数的区间分布,定出了这样一个衡量流畅度的标准:



![image.png](https://img-blog.csdnimg.cn/img_convert/037d3651462bf1f2ee91e0e51374e837.png)



如果一个页面的掉帧数是0~3帧区间,认为是best级别。也就是每一帧的渲染时间基本维持在16ms ~ 48ms之间。转换一下更好理解:



![image.png](https://img-blog.csdnimg.cn/img_convert/dad9fae1c5e8bfe77535d1a4df3c5884.png)



这么定义有什么好处呢? 如果单纯的去看界面的平均帧率,看不出来哪里卡顿了。再有就算每一帧是32ms,只要保持连续帧都是32ms,那么对人眼来说也不认为是卡顿。只有突然出现一帧如 >300ms,那么用户就会觉得不流畅。



因此,我们通过收集掉帧数,来评判一个界面在一段时间内(matrix认为这段时间是200帧。也就是每200帧做一次分析)的掉帧数分布。官网图:



![image.png](https://img-blog.csdnimg.cn/img_convert/f00b68ee2fd3e88ea39e47ba7da71d98.png)



### []( )2.1.2 数据处理逻辑



如果开启了fps开关,那么就开始分析数据吧



//这个是开了fpsmonitor开关才会走这里,否则是默认是走else

listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,

    intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); 



#### []( )2.1.2.1 IDoFrameListener.collect() 方法



`FPSCollector` 继承了 `IDoFrameListener`类,所以先看下`collect`方法:



@CallSuper

public void collect(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame,

                long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {

//FrameReplay 代表每一帧的信息

FrameReplay replay = FrameReplay.create();

replay.focusedActivity = focusedActivity;

replay.startNs = startNs;

replay.endNs = endNs;

replay.dropFrame = dropFrame;

replay.isVsyncFrame = isVsyncFrame;

replay.intendedFrameTimeNs = intendedFrameTimeNs;

replay.inputCostNs = inputCostNs;

replay.animationCostNs = animationCostNs;

replay.traversalCostNs = traversalCostNs;

list.add(replay);



//当收集到的帧数> 200的 且 有提供线程执行器的时候,开始分析收集到的帧数信息

if (list.size() >= intervalFrame && getExecutor() != null) {



// intervalFrame 由 FrameTrace 的内部类 FPSCollector 来决定。



    final List<FrameReplay> copy = new LinkedList<>(list);

    list.clear();

    //拷贝一份,并且根据备份开启一个分析的task。

    getExecutor().execute(new Runnable() {

        @Override

        public void run() {

            doReplay(copy);

            for (FrameReplay record : copy) {

                //处理完成后,进行释放

                record.recycle(); //循环利用

            }

        }

    });

}

}




很简单,注释里写的清楚了。 接着看 `doReplay` 方法:



#### []( )2.1.2.2 FPSCollector.doReplay() 方法



@Override

public void doReplay(List list) {

// 开始分析回放帧集合

super.doReplay(list);



for (FrameReplay replay : list) {

    doReplayInner(replay.focusedActivity, replay.startNs, replay.endNs, replay.dropFrame, replay.isVsyncFrame,

            replay.intendedFrameTimeNs, replay.inputCostNs, replay.animationCostNs, replay.traversalCostNs);

}

}




遍历帧集合,开始对每一帧进行分析~



public void doReplayInner(String visibleScene, long startNs, long endNs, int droppedFrames,

                          boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs,

                          long animationCostNs, long traversalCostNs) {



    if (Utils.isEmpty(visibleScene)) return;

    // 1, 这里需要关注,回答了之前提出的问题

    if (!isVsyncFrame) return;



    // 2,从页面维度,把每一帧和当前的页面关联起来

    FrameCollectItem item = map.get(visibleScene);

    if (null == item) {

        item = new FrameCollectItem(visibleScene);

        map.put(visibleScene, item);

    }

    //3,开始对每个页面的所属帧信息进行收集

    item.collect(droppedFrames);



    

    //4,某个页面触发上报逻辑 timeSliceMs 默认是10s,可配置

    if (item.sumFrameCost >= timeSliceMs) { // report

        map.remove(visibleScene);

       

        // 5, 开始上报

        item.report();

    }

}

}




主要做了5个方面的事情:



1.  排除无效帧

2.  针对页面维度进行收集

3.  具体收集逻辑

4.  上报触发的逻辑

5.  上报整合



*   1、排除无效帧



这里过滤掉非UI绘制消息(无效帧)。比如,你在主线程通过handler发送一个普通的消息,但是并没有去刷新UI,虽然matrix会当做一帧来统计,但是这里不会进行分析。因为根本就没有走绘制流程,没有意义。



**这也就回答之前提出的问题,所以不会影响到数据的统计!**



**还有就是matrix是通过`looper`的消息结束的时候进行监听的,而不是在`choreographer`的`onVsync`方法中去监听。为什么?**



因为 onVsync只能监听到有卡顿,而不知道哪里卡顿了。 而matrix通过自己在外部不断postcallback到choreographer的队列中,只要发生了UI绘制,那么就肯定会走looper的消息结束接口,然后在计算每个阶段的以及当前帧的耗时。非常的巧妙呀!



*   2、针对页面维度进行收集



收集到的帧,肯定有多个帧属于同一个页面的。因此,基于页面的维度来进行统计。 而不是从帧的维度进行统计。这样更难全面的反映页面的卡顿情况分布。



*   3、具体收集逻辑



void collect(int droppedFrames) {

float frameIntervalCost = 1f * UIThreadMonitor.getMonitor().getFrameIntervalNanos()

        / Constants.TIME_MILLIS_TO_NANO;

//统计的时间内,这个页面的总的绘制耗时。

sumFrameCost += (droppedFrames + 1) * frameIntervalCost;



//统计的时间内,这个页面的总的掉帧数 

sumDroppedFrames += droppedFrames;



//统计的时间内,这个页面总共绘制并统计了多少帧 

sumFrame++;



// 这相当于是一个页面的所有统计。

//说白了,就是统计在某个页面,出现了好、坏、中、高等掉帧数的一个分布。看看占比。非常的nice呀!

if (droppedFrames >= frozenThreshold) {

    //发生冻帧的次数,可以用来算占比情况

    dropLevel[DropStatus.DROPPED_FROZEN.index]++;

    // 发生冻帧的 掉帧数,可用来看分布情况 

    dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;

} else if (droppedFrames >= highThreshold) {

    dropLevel[DropStatus.DROPPED_HIGH.index]++;

    dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;

} else if (droppedFrames >= middleThreshold) {

    dropLevel[DropStatus.DROPPED_MIDDLE.index]++;

    dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;

} else if (droppedFrames >= normalThreshold) {

    dropLevel[DropStatus.DROPPED_NORMAL.index]++;

    dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames;

} else {

    dropLevel[DropStatus.DROPPED_BEST.index]++;

    dropSum[DropStatus.DROPPED_BEST.index] += Math.max(droppedFrames, 0);

}

}




注释写的很清楚啦~



*   4、上报触发的逻辑



当前某个页面的总绘制耗时,大于某个阈值才进行上报。 matrix 默认是10s,外部可以配置。



相当于某个页面存在卡顿的情况,但是如果不是经常的触发,导致累计达不到上报的条件,可以认为该页面卡顿情况还是可以接受的。如果触发了,则表示该卡顿大概率存在于线上场景且比较严重,我们需要引起重视了。



*   5、上报整合



void report() {

// 当前页面的实际fps: 每秒多少帧 

float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost);

MatrixLog.i(TAG, "[report] FPS:%s %s", fps, toString());



try {

    TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);

    if (null == plugin) {

        return;

    }

    // 上报每个区间的 掉帧次数,用来算占比

    JSONObject dropLevelObject = new JSONObject();

    dropLevelObject.put(DropStatus.DROPPED_FROZEN.name(), dropLevel[DropStatus.DROPPED_FROZEN.index]);

    dropLevelObject.put(DropStatus.DROPPED_HIGH.name(), dropLevel[DropStatus.DROPPED_HIGH.index]);

    dropLevelObject.put(DropStatus.DROPPED_MIDDLE.name(), dropLevel[DropStatus.DROPPED_MIDDLE.index]);

    dropLevelObject.put(DropStatus.DROPPED_NORMAL.name(), dropLevel[DropStatus.DROPPED_NORMAL.index]);

    dropLevelObject.put(DropStatus.DROPPED_BEST.name(), dropLevel[DropStatus.DROPPED_BEST.index]);



    //上报每个区间的 掉帧数和 用来看分布

    JSONObject dropSumObject = new JSONObject();

    dropSumObject.put(DropStatus.DROPPED_FROZEN.name(), dropSum[DropStatus.DROPPED_FROZEN.index]);

    dropSumObject.put(DropStatus.DROPPED_HIGH.name(), dropSum[DropStatus.DROPPED_HIGH.index]);

    dropSumObject.put(DropStatus.DROPPED_MIDDLE.name(), dropSum[DropStatus.DROPPED_MIDDLE.index]);

    dropSumObject.put(DropStatus.DROPPED_NORMAL.name(), dropSum[DropStatus.DROPPED_NORMAL.index]);

    dropSumObject.put(DropStatus.DROPPED_BEST.name(), dropSum[DropStatus.DROPPED_BEST.index]);



    JSONObject resultObject = new JSONObject();

    resultObject = DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication());

    //上报 当前界面的名字+ 卡顿区间发生次数+卡顿区间掉帧总数+实际fps

    resultObject.put(SharePluginInfo.ISSUE_SCENE, visibleScene);

    resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);

    resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);

    resultObject.put(SharePluginInfo.ISSUE_FPS, fps);



    Issue issue = new Issue();

    issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS);

    issue.setContent(resultObject);

    plugin.onDetectIssue(issue);



} catch (JSONException e) {

    MatrixLog.e(TAG, "json error", e);

} finally {

    sumFrame = 0;

    sumDroppedFrames = 0;

    sumFrameCost = 0;

}

}




[]( )三、总结

====================================================================



`FrameTrace` 做的事情如下:



1.  收集每一帧的信息:包含绘制开始和结束的时间、当前页面名字、input、animation、traversal阶段的耗时等等

2.  从页面的维度触发,对当前页面发生的渲染帧进行了一个全面的评估:卡顿区间的占比是多少。该页面卡顿引起的原因是连续的掉帧? 还是某个帧严重卡顿?

3.  当然,还提供了fps相关的页面展示类: FrameDecorator。在测试环境下,外部可以直接用。



好了,FrameTrace分析完了,后续的分析还在继续!



由于个人水平有限,若有问题之处还望评论指正,大家共同进步!如果觉得文章有帮助,还望帮忙点点赞,感谢~  

···



fps);

    Issue issue = new Issue();

    issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS);

    issue.setContent(resultObject);

    plugin.onDetectIssue(issue);



} catch (JSONException e) {

    MatrixLog.e(TAG, "json error", e);

} finally {

    sumFrame = 0;

    sumDroppedFrames = 0;

    sumFrameCost = 0;

}

}




[]( )三、总结

====================================================================



`FrameTrace` 做的事情如下:



1.  收集每一帧的信息:包含绘制开始和结束的时间、当前页面名字、input、animation、traversal阶段的耗时等等

2.  从页面的维度触发,对当前页面发生的渲染帧进行了一个全面的评估:卡顿区间的占比是多少。该页面卡顿引起的原因是连续的掉帧? 还是某个帧严重卡顿?

3.  当然,还提供了fps相关的页面展示类: FrameDecorator。在测试环境下,外部可以直接用。



好了,FrameTrace分析完了,后续的分析还在继续!



由于个人水平有限,若有问题之处还望评论指正,大家共同进步!如果觉得文章有帮助,还望帮忙点点赞,感谢~



# 最后

那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。

> 通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!
>
> 北京 Android 工程师「工资收入水平 」

![](https://img-blog.csdnimg.cn/img_convert/9f3faf534340a10ed6d77e522cb84217.webp?x-oss-process=image/format,png)

今天重点内容是怎么去学,怎么提高自己的技术。

1.合理安排时间

2.找对好的系统的学习资料

3.有老师带,可以随时解决问题

4.有明确的学习路线


当然图中有什么需要补充的或者是需要改善的,可以在评论区写下来,一起交流学习。

![](https://img-blog.csdnimg.cn/img_convert/f636941aaa2de357a78d0e5f11458f3c.webp?x-oss-process=image/format,png)



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

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

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


        sumFrameCost = 0;

    }

} 



三、总结

====================================================================

FrameTrace 做的事情如下:

  1. 收集每一帧的信息:包含绘制开始和结束的时间、当前页面名字、input、animation、traversal阶段的耗时等等

  2. 从页面的维度触发,对当前页面发生的渲染帧进行了一个全面的评估:卡顿区间的占比是多少。该页面卡顿引起的原因是连续的掉帧? 还是某个帧严重卡顿?

  3. 当然,还提供了fps相关的页面展示类: FrameDecorator。在测试环境下,外部可以直接用。

好了,FrameTrace分析完了,后续的分析还在继续!

由于个人水平有限,若有问题之处还望评论指正,大家共同进步!如果觉得文章有帮助,还望帮忙点点赞,感谢~

最后

那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。

通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!

北京 Android 工程师「工资收入水平 」

[外链图片转存中…(img-u97wIOBC-1715818428022)]

今天重点内容是怎么去学,怎么提高自己的技术。

1.合理安排时间

2.找对好的系统的学习资料

3.有老师带,可以随时解决问题

4.有明确的学习路线

当然图中有什么需要补充的或者是需要改善的,可以在评论区写下来,一起交流学习。

[外链图片转存中…(img-MXaGkW1Y-1715818428022)]

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值