Android 性能优化“基石”是什么? Fps,Memory,Cpu如何采集?

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

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

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注Android)
img

正文

}

final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,

// and keep track of what that identity token actually is.

Binder.clearCallingIdentity();

final long ident = Binder.clearCallingIdentity();

// Allow overriding a threshold with a system prop. e.g.

// adb shell ‘setprop log.looper.1000.main.slow 1 && stop && start’

final int thresholdOverride =

SystemProperties.getInt(“log.looper.”

  • Process.myUid() + “.”

  • Thread.currentThread().getName()

  • “.slow”, 0);

boolean slowDeliveryDetected = false;

for (;😉 {

Message msg = queue.next(); // might block

if (msg == null) {

// No message indicates that the message queue is quitting.

return;

}

// This must be in a local variable, in case a UI event sets the logger

final Printer logging = me.mLogging;

if (logging != null) {

logging.println(">>>>> Dispatching to " + msg.target + " " +

msg.callback + ": " + msg.what);

}

if (logging != null) {

logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

}

}

}

从源码上我们可以看到Looper一开始就预留了一个Printer的类,其中在Message执行开始和Message执行结束之后都会执行Printer方法。我们可以通过 looper.setMessageLogging(new LooperPrinter());的方法来设置这个监控。

IdleHandler

当Looper中的MessageQueue为空的情况下,会触发IdleHandler,所以主线程卡顿,一般都会配合这个一起来重置耗时时间,这样就能保证主线程空置的情况下,方法耗时不会计算出错。

UIThreadMonitor(主线程监控)

简单的介绍了下上面几个东西之后,我们的Fps采集的这部分实际的采样代码我参考了下Matrix的UIThreadMonitor,而UIThreadMonitor则是通过上述几个组合的方式来完成的。

private void dispatchEnd() {

long traceBegin = 0;

if (config.isDevEnv()) {

traceBegin = System.nanoTime();

}

long startNs = token;

long intendedFrameTimeNs = startNs;

if (isVsyncFrame) {

doFrameEnd(token);

intendedFrameTimeNs = getIntendedFrameTimeNs(startNs);

}

long endNs = System.nanoTime();

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]);

}

}

}

dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();

dispatchTimeMs[1] = System.nanoTime();

AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);

synchronized (observers) {

for (LooperObserver observer : observers) {

if (observer.isDispatchBegin()) {

observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isVsyncFrame);

}

}

}

this.isVsyncFrame = false;

if (config.isDevEnv()) {

MatrixLog.d(TAG, “[dispatchEnd#run] inner cost:%sns”, System.nanoTime() - traceBegin);

}

}

UIThreadMonitor则不太一样,其中dispatchEnd方法有其中LooperMonitor所接受到的。

而LooperMonitor他通过主线程的Looper的setMessageLogging方法设置一个LooperPrinter。dispatchEnd在主线程的方法执行结束之后,通过反射Choreographer获取当前的绘制的Vsync和渲染时长。最后当IdleHandler被触发的时候,则重置LooperPrinter时间的方式,从而避免主线程闲置状况下方法耗时计算出问题。

这部分源代的传送门Matrix LoopMonitor

https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/LooperMonitor.java

为什么要绕一个大圈子来监控Fps呢?这么写的好处是什么呢?

我特地去翻查了下Matrix官方的wiki *https://github.com/Tencent/matrix/wiki/Matrix-Android-TraceCanary *,Martix参考了BlockCanary的代码,通过结合了下Choreographer和BlockCanary,当出现卡顿帧的时候获取当前的主线程卡顿的堆栈,然后通过LooperPrinter把当前的卡顿的堆栈方法输出,这样可以更好的辅助开发去定位卡顿问题,而不是直接告诉业务方你的页面卡顿了。

采样分析

文章开始抛出过一个问题,如果采集的每一个数据都上报首先会对服务器产生巨大的无效数据压力,其次也会有很多无效的数据上报,那么应该怎么做呢?

这一块我们参考了Matrix的代码,首先Fps数据不可能是实时上报的,其次最好能从一个时间段内的数据中筛选出有问题的数据,Matrix的Fps采集的有几个小细节其实做的很好。

  1. 延迟200毫秒.先收集200帧的数据,然后对其数据内容进行分析,筛选遍历出最大帧最小帧,以及平均帧,之后内存保存数据。

  2. 子线程处理数据,筛选遍历的操作移动到子线程,这样避免APM反倒造成App卡顿问题。

  3. 200毫秒的数据只是作为其中一个数据片段,Matrix的上报节点是以一个更长的时间段作为上报的,当时间超过1分钟左右的情况下,才会作为一个Issue片段上报。

  4. 前后台切换状态并不需要采集数据。

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);

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()) {

if (listener.getIntervalFrameReplay() > 0) {

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

intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

} else {

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

@Override

public void run() {

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

intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

}

});

}

} else {

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

intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);

}

}

}

}

}

上面是Matirx的源代码,其中我们可以看出listener.getIntervalFrameReplay() > 0当这个条件触发的情况下,listener会先做一次collection操作,当触发到一定的数据量之后,才会触发后续的逻辑。其次我们可以看到判断了null != listener.getExecutor(),所以这部分收集的操作被执行在线程池中。

private class FPSCollector extends IDoFrameListener {

private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper());

Executor executor = new Executor() {

@Override

public void execute(Runnable command) {

frameHandler.post(command);

}

};

private HashMap<String, FrameCollectItem> map = new HashMap<>();

@Override

public Executor getExecutor() {

return executor;

}

@Override

public int getIntervalFrameReplay() {

return 200;

}

@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;

if (!isVsyncFrame) return;

FrameCollectItem item = map.get(visibleScene);

if (null == item) {

item = new FrameCollectItem(visibleScene);

map.put(visibleScene, item);

}

item.collect(droppedFrames);

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

map.remove(visibleScene);

item.report();

}

}

}

private class FrameCollectItem {

String visibleScene;

long sumFrameCost;

int sumFrame = 0;

int sumDroppedFrames;

// record the level of frames dropped each time

int[] dropLevel = new int[DropStatus.values().length];

int[] dropSum = new int[DropStatus.values().length];

FrameCollectItem(String visibleScene) {

this.visibleScene = visibleScene;

}

void collect(int droppedFrames) {

float frameIntervalCost = 1f * UIThreadMonitor.getMonitor().getFrameIntervalNanos() / Constants.TIME_MILLIS_TO_NANO;

sumFrameCost += (droppedFrames + 1) * frameIntervalCost;

sumDroppedFrames += droppedFrames;

sumFrame++;

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);

}

}

}

这部分代码则是Matrix对于一个帧片段进行数据处理的逻辑。可以看出,collect方法内筛选出了最大最小等等多个纬度的数据,丰富了一个数据片段。这个地方数据越多越能帮助一个开发定位问题。

采集逻辑也参考了Matrix的这部分代码,但是在实际测试阶段发现了个小Bug,因为上报的是一个比较大的时间片段,用户切换了页面之后,会把上个页面的fps数据也当做下个页面的数据上报。

所以我们增加了一个ActivityLifeCycle,当页面发生变化的情况下进行一次数据上报操作。其次我们把Matrix内的前后台切换等逻辑也进行了一次调整,更换成更可靠的ProcessLifecycleOwner。

4 Cpu和Memory


内存和Cpu的使用状况可以更好的帮我们检测线上用户的真实情况,而不是等到用户crash之后我们再去反推这个问题,可以根据页面维度筛选出不同的页面数据,方便开发分析对应的问题。

在已经获取到Fps的经验之后,我们在这个基础上增加了Cpu和Memory的数据收集。相对来说我们可以借鉴大量的采集逻辑,然后只要在获取关键性数据进行调整就好了。

  1. 数据在子线程中采集,避免采集数据卡顿主线程。

  2. 同时每秒采集一次数据,数据内容本地分析,计算峰值谷值均值

  3. 数据上报节点拆分,一定时间内,页面切换,生成一个数据。

  4. 合并Cpu和内存数据,作为同一个数据结构上报,优化数据流量问题。

Memory 数据采集

Memory的数据我们参考了下Dokit的代码,高低版本也有差异,高版本可以直接通过Debug.MemoryInfo()获取到内存的数据,低版本则需要通过ams获取到ActivityManager从中获取数据。

以下是性能采集的工具类同时采集了cpu数据,各位可以直接使用。

object PerformanceUtils {

private var CPU_CMD_INDEX = -1

@JvmStatic

fun getMemory(): Float {

val mActivityManager: ActivityManager? = Hasaki.getApplication().getSystemService(Context.ACTIVITY_SERVICE)

as ActivityManager?

var mem = 0.0f

try {

最后

由于文章篇幅原因,我只把面试题列了出来,详细的答案,我整理成了一份PDF文档,这份文档还包括了还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 ,帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习。

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

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
img

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

te var CPU_CMD_INDEX = -1

@JvmStatic

fun getMemory(): Float {

val mActivityManager: ActivityManager? = Hasaki.getApplication().getSystemService(Context.ACTIVITY_SERVICE)

as ActivityManager?

var mem = 0.0f

try {

最后

由于文章篇幅原因,我只把面试题列了出来,详细的答案,我整理成了一份PDF文档,这份文档还包括了还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 ,帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习。

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

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-2wrxrsQF-1713678574476)]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值