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采集的有几个小细节其实做的很好。
-
延迟200毫秒.先收集200帧的数据,然后对其数据内容进行分析,筛选遍历出最大帧最小帧,以及平均帧,之后内存保存数据。
-
子线程处理数据,筛选遍历的操作移动到子线程,这样避免APM反倒造成App卡顿问题。
-
200毫秒的数据只是作为其中一个数据片段,Matrix的上报节点是以一个更长的时间段作为上报的,当时间超过1分钟左右的情况下,才会作为一个Issue片段上报。
-
前后台切换状态并不需要采集数据。
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。
内存和Cpu的使用状况可以更好的帮我们检测线上用户的真实情况,而不是等到用户crash之后我们再去反推这个问题,可以根据页面维度筛选出不同的页面数据,方便开发分析对应的问题。
在已经获取到Fps的经验之后,我们在这个基础上增加了Cpu和Memory的数据收集。相对来说我们可以借鉴大量的采集逻辑,然后只要在获取关键性数据进行调整就好了。
-
数据在子线程中采集,避免采集数据卡顿主线程。
-
同时每秒采集一次数据,数据内容本地分析,计算峰值谷值均值
-
数据上报节点拆分,一定时间内,页面切换,生成一个数据。
-
合并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 {
var memInfo: Debug.MemoryInfo? = null
if (Build.VERSION.SDK_INT > 28) {
// 统计进程的内存信息 totalPss
memInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(memInfo)
} else {
//As of Android Q, for regular apps this method will only return information about the memory info for the processes running as the caller’s uid;
// no other process memory info is available and will be zero. Also of Android Q the sample rate allowed by this API is significantly limited, if called faster the limit you will receive the same data as the previous call.
val memInfos = mActivityManager?.getProcessMemoryInfo(intArrayOf(Process.myPid()))
memInfos?.firstOrNull()?.apply {
memInfo = this
}
}
memInfo?.apply {
val totalPss = totalPss
if (totalPss >= 0) {
mem = totalPss / 1024.0f
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return mem
}
/**
-
8.0以下获取cpu的方式
-
@return
*/
private fun getCPUData(): String {
val commandResult = ShellUtils.execCmd(“top -n 1 | grep ${Process.myPid()}”, false)
val msg = commandResult.successMsg
return try {
msg.split(“\s+”.toRegex())[CPU_CMD_INDEX]
} catch (e: Exception) {
“0.5%”
}
}
@WorkerThread
fun getCpu(): String {
if (CPU_CMD_INDEX == -1) {
getCpuIndex()
}
if (CPU_CMD_INDEX == -1) {
return “”
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getCpuDataForO()
} else {
getCPUData()
}
}
/**
-
8.0以上获取cpu的方式
-
@return
*/
private fun getCpuDataForO(): String {
return try {
val commandResult = ShellUtils.execCmd(“top -n 1 | grep ${Process.myPid()}”, false)
var cpu = 0F
commandResult.successMsg.split(“\n”).forEach {
val cpuTemp = it.split(“\s+”.toRegex())
val cpuRate = cpuTemp[CPU_CMD_INDEX].toFloatOrNull()?.div(Runtime.getRuntime()
.availableProcessors())?.div(100) ?: 0F
cpu += cpuRate
}
NumberFormat.getPercentInstance().format(cpu)
} catch (e: Exception) {
“”
}
}
private fun getCpuIndex() {
try {
val process = Runtime.getRuntime().exec(“top -n 1”)
val reader = BufferedReader(InputStreamReader(process.inputStream))
var line: String? = null
while (reader.readLine().also { line = it } != null) {
line?.let {
line = it.trim { it <= ’ ’ }
line?.apply {
val tempIndex = getCPUIndex(this)
if (tempIndex != -1) {
CPU_CMD_INDEX = tempIndex
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
先自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以扫码领取!!!!
最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;
我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言
高级UI与自定义view;
自定义view,Android开发的基本功。
性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
NDK开发;
未来的方向,高薪必会。
前沿技术;
组件化,热升级,热修复,框架设计
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可免费领取!
语言与原理;**
大厂,小厂。Android面试先看你熟不熟悉Java语言
[外链图片转存中…(img-ankkFaHX-1711219332644)]
高级UI与自定义view;
自定义view,Android开发的基本功。
[外链图片转存中…(img-MYj1bJZF-1711219332645)]
性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
[外链图片转存中…(img-0p9q9Hye-1711219332645)]
NDK开发;
未来的方向,高薪必会。
[外链图片转存中…(img-3vchqG29-1711219332645)]
前沿技术;
组件化,热升级,热修复,框架设计
[外链图片转存中…(img-nvwW0lOu-1711219332645)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可免费领取!