堆栈信息采集
进行性能分析的时候,如检测到卡顿,ANR等异常指标时,需要还原现场来进行问题的追踪,因此知道如何获取当前的程序的调用堆栈信息来还原现场十分的重要。
记录调用堆栈信息的方式大致可通过以下两种方式获得
定时器抓取堆栈
启动一个定时器,一定时间间隔进行堆栈抓取,按照时间戳存储在堆栈信息列表中。当发生问题时,根据当前时间戳从堆栈信息列表中截取一段时间段的堆栈信息。
这种方式实现简单稳定,但可能会存在一定的误差。
private static final LinkedHashMap<Long, String> stackMap = new LinkedHashMap<>();
...
//抓取堆栈信息
StringBuilder stringBuilder = new StringBuilder(4096);
for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) {
stringBuilder.append(stackTraceElement.toString())
.append("\r\n");
}
stackMap.put(System.currentTimeMillis(), stringBuilder.toString());
插桩记录方法队列
通过Gradle Transfrom API在编译的过程中去修改字节码进行插桩处理,在每个类的每个方法开始和结束位置注入指定代码用于收集该方法的名称和耗时并存储到全局的队列中。当发生问题时,截取队列最近的几条记录作为堆栈信息。
这种方式能够准确的定位问题函数,但是插桩会增加包的大小和不稳定性。
以下为Tencent Matrix中对于方法插桩的实现
- MatrixTraceTransform#transform()
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
...
//定义方法id和方法名映射文件
final MappingCollector mappingCollector = new MappingCollector()
File mappingFile = new File(traceConfig.getMappingPath());
if (mappingFile.exists() && mappingFile.isFile()) {
MappingReader mappingReader = new MappingReader(mappingFile);
mappingReader.read(mappingCollector)
}
//收集源码中的类和三方jar包的类
Map<File, File> jarInputMap = new HashMap<>()
Map<File, File> scrInputMap = new HashMap<>()
transformInvocation.inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput dirInput ->
collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
}
input.jarInputs.each { JarInput jarInput ->
if (jarInput.getStatus() != Status.REMOVED) {
collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
}
}
}
//将所有类中的方法收集起来,生成方法id和方法名映射,并保存在mCollectedMethodMap中
MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
HashMap<String, TraceMethod> mCollectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
//处理所有的类对其进行插桩
MethodTracer methodTracer = new MethodTracer(traceConfig, mCollectedMethodMap, methodCollector.getCollectedClassExtendMap())
methodTracer.trace(scrInputMap, jarInputMap)
origTransform.transform(transformInvocation)
Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start)
}
- MethodTracer#trace() -> MethodTracer#traceMethodFromSrc() -> MethodTracer#innerTraceMethodFromSrc()
private void innerTraceMethodFromSrc(File input, File output) {
...
// 通过asm库对类的字节码进行修改
is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
is.close();
if (output.isDirectory()) {
os = new FileOutputStream(changedFileOutput);
} else {
os = new FileOutputStream(output);
}
os.write(classWriter.toByteArray());
os.close();
}
- MethodTracer.TraceClassAdapter
private class TraceClassAdapter extends ClassVisitor {
...
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
...
//在类中每个方法的节点,通过asm库对方法的字节码进行修改
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
hasWindowFocusMethod, isMethodBeatClass);
}
...
}
- MethodTracer.TraceMethodAdapter
private class TraceMethodAdapter extends AdviceAdapter {
...
@Override
protected void onMethodEnter() {
//在方法的开始节点,通过asm库插入com.tencent.matrix.trace.core.MethodBeat.i()代码
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
//在方法的结束节点,通过asm库插入com.tencent.matrix.trace.core.MethodBeat.o()代码
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
...
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
}
}
com.tencent.matrix.trace.core.MethodBeat.i()和com.tencent.matrix.trace.core.MethodBeat.o()两段代码即用来统计每个方法的信息。
帧率采集
Android提供的帧率采集的方式大致上有两种,设置Choreographer.FrameCallback回调和OnFrameMetricsAvailableListener回调。
OnFrameMetricsAvailableListener回调数据源更加丰富,同时要求的API版本也较高。
Choreographer.FrameCallback
通过Choreographer.FrameCallback监听器,监听每一帧回调doFrame()。一般设备刷新频率在60HZ,即每帧间隔在16.66ms左右。
final Choreographer mChoreographer = Choreographer.getInstance();
mChoreographer.postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//统计每帧时间,单位纳秒
long interval = frameTimeNanos - lastTime;
lastTime = frameTimeNanos;
mChoreographer.postFrameCallback(this);
}
});
OnFrameMetricsAvailableListener
从Android 7.0(API 24)开始,Android SDK新增OnFrameMetricsAvailableListener接口用于提供帧绘制各阶段的耗时,数据源与 GPU Profile 相同。能够描述每帧各阶段的耗时。
if(Build.VERSION.SDK_INT >= 24) {
getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() {
@Override
public void onFrameMetricsAvailable(Window window, FrameMetrics metrics, int dropCountSinceLastInvocation) {
Log.d("Metrics", "动画耗时: " + metrics.getMetric((FrameMetrics.ANIMATION_DURATION)) / Math.pow(10, 6));
Log.d("Metrics", "执行 OpenGL 命令和 DisplayList 耗时: " + metrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "创建和更新 DisplayList 耗时: " + metrics.getMetric(FrameMetrics.DRAW_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "布尔值,标志该帧是否为此 Window 绘制的第一帧: " + metrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) / Math.pow(10, 6));
Log.d("Metrics", "处理用户输入操作的耗时: " + metrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "layout/measure 耗时: " + metrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "CPU 在等待 GPU 完成渲染的耗时: " + metrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "上传 bitmap 到 GPU 的耗时: " + metrics.getMetric(FrameMetrics.SYNC_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "整帧渲染耗时: " + metrics.getMetric(FrameMetrics.TOTAL_DURATION) / Math.pow(10, 6));
Log.d("Metrics", "未知延迟: " + metrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION) / Math.pow(10, 6));
// 日志输出,单位毫秒
// D/Metrics: 动画耗时: 0.499687
// D/Metrics: 执行 OpenGL 命令和 DisplayList 耗时: 1.033854
// D/Metrics: 创建和更新 DisplayList 耗时: 2.467813
// D/Metrics: 布尔值,标志该帧是否为此 Window 绘制的第一帧: 0.0
// D/Metrics: 处理用户输入操作的耗时: 0.11
// D/Metrics: layout/measure 耗时: 0.448698
// D/Metrics: CPU 在等待 GPU 完成渲染的耗时: 0.537656
// D/Metrics: 上传 bitmap 到 GPU 的耗时: 0.068906
// D/Metrics: 整帧渲染耗时: 10.150618
// D/Metrics: 未知延迟: 4.950827
}
}, new Handler());
}
UI卡顿、ANR采集
UI卡顿和ANR的原因都是主线程进行耗时操作导致的帧率丢失,因此可以通过监听帧率回调,或者监听主线程消息处理两种方式进行采集。
- 监听帧率回调间隔超过指定阈值
通过Choreographer.FrameCallback回调,当间隔超过不同的阈值则认定为卡顿或ANR(一般卡顿1s,ANR 5s),并且抓取堆栈信息进行输出。
- 主线消息处理时间超过指定阈值
通过android.util.Printer对主线程每条消息处理进行打印,通过计算>>>>>和<<<<<两条日志之间的时间戳得到主线程处理每条消息的时间。某条消息处理时间超过不同时长可认定为卡顿或ANR
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
//主线程Looper每从MessageQueue处理一条msg都会回调两次,日志格式 ">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what
//>>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {3fac4cc} android.view.View$UnsetPressedState@b842315: 0
//<<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {3fac4cc} android.view.View$UnsetPressedState@b842315
//>>>>> Dispatching to Handler (android.app.ActivityThread$H) {bef393} null: 104
//<<<<< Finished to Handler (android.app.ActivityThread$H) {bef393} null
//>>>>> Dispatching to Handler (android.os.Handler) {9fae164} null: 1
//<<<<< Finished to Handler (android.os.Handler) {9fae164} null
}
});
内存信息采集
Android内存几个指标的含义如下:
Item | 全称 | 含义 | 等价 |
---|---|---|---|
USS | Unique Set Size | 物理内存 | 进程独占的内存 |
PSS | Proportional Set Size | 物理内存 | PSS= USS+ 按比例包含共享库 |
RSS | Resident Set Size | 物理内存 | RSS= USS+ 包含共享库 |
VSS | Virtual Set Size | 虚拟内存 | VSS= RSS+ 未分配实际物理内存 |
故内存的大小关系:VSS >= RSS >= PSS >= USS
一般统计应用进程的内存以PSS值为主
Android中获取内存的几种不同方法
- ActivityManager.MemoryInfo
主要是用于得到当前系统剩余内存的及判断是否处于低内存运行
ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
((ActivityManager) getSystemService(ACTIVITY_SERVICE)).getMemoryInfo(info);
- Debug.MemoryInfo
Debug.MemoryInfo[] memInfos = activityManager.getProcessMemoryInfo(new int[]{pid1, pid2});
Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); Debug.getMemoryInfo(memInfo);
描述内存使用情况比较详细数据,单位KB。前者获得指定进程内存使用情况,后者获得当前进程内存。其数据也可通过adb shell "dumpsys meminfo com.xxx.xxx"查看指定进程名得到
- Debug#getNativeHeapSize() getNativeHeapAllocatedSize() getNativeHeapFreeSize()
得到Native的内存大概情况,单位B。分别是native堆已使用内存,剩余内存和native堆本身内存大小
Android内存警告采集
OnLowMemory是Android提供的API,在系统内存不足,所有后台程序(优先级为background的进程,不是指后台运行的进程)都被杀死时,系统会调用OnLowMemory。
OnTrimMemory是Android 4.0 (API1.6)之后提供的API,系统会根据不同的内存状态来回调。根据不同的内存状态,来响应不同的内存释放策略。
getApplication().registerComponentCallbacks(new ComponentCallbacks2() {
@Override
public void onTrimMemory(int level) {
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
}
@Override
public void onLowMemory() {
}
});
onTrimMemory(int level) level不同值代表不同状态:
static final int TRIM_MEMORY_COMPLETE = 80;
static final int TRIM_MEMORY_MODERATE = 60;
static final int TRIM_MEMORY_BACKGROUND = 40;
static final int TRIM_MEMORY_UI_HIDDEN = 20;
static final int TRIM_MEMORY_RUNNING_CRITICAL = 15;
static final int TRIM_MEMORY_RUNNING_LOW = 10;
static final int TRIM_MEMORY_RUNNING_MODERATE = 5;
数值越大一般表示系统内存状态越紧张
5 10 15表示该进程是重要进程,系统处于不同低内存状态,需要应用进行相应回收操作。
20 表示进程用户界面消失,退到后台
40 60 80 表示该进程是后台进程,在低内存状态时处于回收LRU列表的位置(即将计入LRU,LRU中部,LRU底部)
CPU的使用率采集
获取应用CPU使用率的步骤如下
- 读取设备cpu总使用情况(从开机为止)
/proc/stat 计算totalCpuTime - 读取当前应用cpu使用情况(从进程开始为止)
/proc/pid/stat 计算processCpuTime - 计算cpu使用率
取一段时间内应用cpu使用时间 / cpu总是用时间
△processCpuTime / △totalCpuTime
android 8(API 26) or higher is inaccessible to /proc/stat
/proc/stat: open failed: EACCES (Permission denied)
只有系统应用才能访问/proc/stat,无法获取设备cpu的总使用情况,只能够获取当前应用的cpu使用情况,目前采取的做法可暂时用简单的两次采集时间间隔来替代cpu总时间进行计算。
页面启动时间采集
页面启动时间即打开页面到完成加载的时间,Activity生命周期提供了相应的回调函数,也可通过Activity消息事件进一步获取。
-
onCreate和onPostCreate回调时间间隔
-
LAUNCH_ACTIVITY和ENTER_ANIMATION_COMPLETE两个事件的时间间隔
onCreate 和 onPostCreate的时间间隔,和用户主观感受到页面打开的时间可能存在着偏差,可能采集时更需要从Activity创建到入场动画结束的时间
查看ActivityThread的源码得知,Activity的执行事件都通过发送消息交由ActivityThread.H类处理
消息类型定义如下:
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public static final int PAUSE_ACTIVITY = 101;
......
public static final int ENTER_ANIMATION_COMPLETE = 149;
......
}
LAUNCH_ACTIVITY表示开始创建Activity,ENTER_ANIMATION_COMPLETE则表示Activity入场动画结束完全呈现Activity。
因此,我们可以通过反射获取到ActivityThread.H的实例对象并且为其设置代理类,在代理类中对 LAUNCH_ACTIVITY 和 ENTER_ANIMATION_COMPLETE 两个消息进行监听计算时间差得到页面启动时间
public static void hackCallback() {
try {
//1. 反射ActivityThread的静态变量sCurrentActivityThread获取ActivityThread实例
Class<?> forName = Class.forName("android.app.ActivityThread");
Field field = forName.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object activityThreadValue = field.get(forName);
//2. 反射获取ActivityThread实例的mH,这是一个继承Hadler的实现类
Field mH = forName.getDeclaredField("mH");
mH.setAccessible(true);
Object handler = mH.get(activityThreadValue);
//3. 反射替换mH的mCallback对象为自己创建的静态代理类HackCallback
Class<?> handlerClass = handler.getClass().getSuperclass();
Field callbackField = handlerClass.getDeclaredField("mCallback");
callbackField.setAccessible(true);
Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
HackCallback callback = new HackCallback(originalCallback);
callbackField.set(handler, callback);
} catch (Exception e) {
}
}
public class HackCallback implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
// TODO
} else if (msg.what == ENTER_ANIMATION_COMPLETE) {
// TODO
}
}
内存泄露采集
内存泄露主要通过定时器不断的去观察队列里检测不再使用的对象是否已经被回收,如果多次检测到某一对象没有回收则认定为该对象内存泄露。
具体的步骤:
- 在对象不使用时将其加入观察队列,以WeakReference形式持有。针对Activity的泄露,可以在onDestroy时将其加入观察队列
- 定时器每隔一段时间遍历观察队列里的对象,主动触发GC
- 如果观察对象WeakReference为空则说明没泄露并将其移除。
- 如果不为空则判断计数是否超过最大次数,没超过则计数加1继续保留在队列,超过则说明已经泄露。
- 如果发生内存泄露则通过android.os.Debug.dumpHprofData(hprofPath) 生成hpro文件进行输出,可以通过此文件对内存泄露进行分析
过度重绘检测
过度重绘更多的针对于开发阶段的布局问题追查,Android提供了一系列工具可以进行查看,同时代码中也可以对View进行分析,帮助我们采集相关问题。
具体的步骤如下:
- 找一特定时机(如生命周期onActDestroyed回调)获取activity.getWindow().getDecorView(),遍历View Tree的对每个View进行分析
- 检测ImageView的图片资源大于ImageView的大小尺寸,给出优化提醒
- 检测View.getBackground()和getDrawable()同时不为空,可能导致过度重绘,给出优化提醒
- 检测View的子View只有一个且大小相同,给出优化提醒
网友评论