360Apm 是什么?
ArgusAPM是360手机卫士客户端团队继RePlugin之后开源的又一个重量级开源项目。ArgusAPM是360移动端产品使用的可视化性能监控平台,为移动端APP提供性能监控与管理,可以迅速发现和定位各类APP性能和使用问题,帮助APP不断的提升用户体验。
项目地址:https://github.com/Qihoo360/ArgusAPM
apm 是如何收集卡顿(block)信息的?
package com.argusapm.android.core.job.block;
/**
* 卡顿模块Task
*
* @author ArgusAPM Team
*/
public class BlockTask extends BaseTask {
private final String SUB_TAG = "BlockTask";
private HandlerThread mBlockThread = new HandlerThread("blockThread");
private Handler mHandler;
private Runnable mBlockRunnable = new Runnable() {
@Override
public void run() {
if (!isCanWork()) {
return;
}
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString() + "\n");
}
if (DEBUG) {
LogX.d(TAG, SUB_TAG, sb.toString());
}
saveBlockInfo(sb.toString());
}
};
@Override
public void start() {
super.start();
if (!mBlockThread.isAlive()) { //防止多次调用
mBlockThread.start();
mHandler = new Handler(mBlockThread.getLooper());
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String x) {
if (x.startsWith(START)) {
startMonitor();
}
if (x.startsWith(END)) {
removeMonitor();
}
}
});
}
}
public void startMonitor() {
mHandler.postDelayed(mBlockRunnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.blockMinTime);
}
public void removeMonitor() {
mHandler.removeCallbacks(mBlockRunnable);
}
}
com.argusapm.android.core.job.block.BlockTask 是收集卡顿信息的类,可以看到,他是拿到了MainLooper(), 然后设置了一个setMessageLogging。Looper.getMainLooper().setMessageLogging
mainLooper 里面执行的都是主线程里面执行的代码。
那为什么设置了一个messageLogging 就可以拿到卡顿信息呢?
我们看下Looper 的loop 方法:
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
}
看可以看到,每一个msg 执行之前都会调用logging.println(">>>>> Dispatching to " + msg.target + " " +
,每一个msg 结束都会执行logging.println("<<<<< Finished to "
所以,360收集卡顿是设置了一个mainLooper 的logging,然后如果log 信息 x.startsWith(“>>>>> Dispatching"),那么我就开启一个延时一定时间的runable,如果msg 里面的执行完成了x.startsWith(“<<<<< Finished”),那么表示结束了,那就移除掉这个延迟的runable, 如果延时的runable 到时间了,还没有打印<<<<< Finished,则表示你在主线程里面,操作的时间太长了,那么这时候,这个延时的runable 里面就会记录当前的堆栈。
这样就做到了收集卡顿的信息。
注:如果是我,我真的想不到可以这样做,反思下:
1.对源码不够了解 – 以后要多看源码
2.思考的太少–以后要多思考
如何收集Okhttp信息?
集成apm 的过程中,有一个gradle 插件。
这个插件里面,回去注册一个android transform
android.registerTransform(ArgusAPMTransform(project))
这样,android 编译apk的时候,会把所有的jar 和 class 都会回调给自己写的transform处理。
会在编译期间,读取所有的class 文件,如果发现class 文件是Okhttp 的类的时候,在 方法里,拿到interceptors 字段,添加上自己的拦截器,就完成了用asm写入一段字节码。 这样就做到了可以采集OkHttp 信息。
具体可以参看源码。
如何收集帧(fps)信息?
package com.argusapm.android.core.job.fps;
public class FpsTask extends BaseTask implements Choreographer.FrameCallback {
private final String SUB_TAG = ApmTask.TASK_FPS;
private long mLastFrameTimeNanos = 0; //最后一次时间
private long mFrameTimeNanos = 0; //本次的当前时间
private int mCurrentCount = 0; //当前采集条数
private int mFpsCount = 0;
private FpsInfo fpsInfo = new FpsInfo();
private JSONObject paramsJson = new JSONObject();
//定时任务
private Runnable runnable = new Runnable() {
@Override
public void run() {
if (!isCanWork()) {
mCurrentCount = 0;
return;
}
calculateFPS();
mCurrentCount++;
//实现分段采集
if (mCurrentCount < ArgusApmConfigManager.getInstance().getArgusApmConfigData().onceMaxCount) {
AsyncThreadTask.executeDelayed(runnable, TaskConfig.FPS_INTERVAL);
} else {
AsyncThreadTask.executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().pauseInterval > TaskConfig.FPS_INTERVAL ? ArgusApmConfigManager.getInstance().getArgusApmConfigData().pauseInterval : TaskConfig.FPS_INTERVAL);
mCurrentCount = 0;
}
}
};
private void calculateFPS() {
if (mLastFrameTimeNanos == 0) {
mLastFrameTimeNanos = mFrameTimeNanos;
return;
}
float costTime = (float) (mFrameTimeNanos - mLastFrameTimeNanos) / 1000000.0F;
if (mFpsCount <= 0 && costTime <= 0.0F) {
return;
}
int fpsResult = (int) (mFpsCount * 1000 / costTime);
if (fpsResult < 0) {
return;
}
if (fpsResult <= TaskConfig.DEFAULT_FPS_MIN_COUNT) {
fpsInfo.setFps(fpsResult);
try {
paramsJson.put(FpsInfo.KEY_STACK, CommonUtils.getStack());
} catch (JSONException e) {
e.printStackTrace();
}
fpsInfo.setParams(paramsJson.toString());
fpsInfo.setProcessName(ProcessUtils.getCurrentProcessName());
save(fpsInfo);
}
if (AnalyzeManager.getInstance().isDebugMode()) {
if (fpsResult > TaskConfig.DEFAULT_FPS_MIN_COUNT) {
fpsInfo.setFps(fpsResult);
}
AnalyzeManager.getInstance().getParseTask(ApmTask.TASK_FPS).parse(fpsInfo);
}
mLastFrameTimeNanos = mFrameTimeNanos;
mFpsCount = 0;
}
@Override
public void start() {
super.start();
AsyncThreadTask.executeDelayed(runnable, (int) (Math.round(Math.random() * TaskConfig.TASK_DELAY_RANDOM_INTERVAL)));
Choreographer.getInstance().postFrameCallback(this);
}
@Override
public void doFrame(long frameTimeNanos) {
mFpsCount++;
mFrameTimeNanos = frameTimeNanos;
if (isCanWork()) {
//注册下一帧回调
Choreographer.getInstance().postFrameCallback(this);
} else {
mCurrentCount = 0;
}
}
}
上面这个是收集fps 的任务类。Choreographer.getInstance().postFrameCallback(this);
这句话,把当前的任务类,添加到绘制一帧frame 的callback 里面。
我们看下这个方法:
/**
* Posts a frame callback to run on the next frame.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @param callback The frame callback to run during the next frame.
*
* @see #postFrameCallbackDelayed
* @see #removeFrameCallback
*/
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}
可以看到,到了绘制的时候,就会调用doFrame 方法。这个callback 执行一次之后,就会被移除掉。所以,他们在doFrame 里面又执行了Choreographer.getInstance().postFrameCallback(this);继续监听。
当到了时间执行采集的runable,就会去计算绘制了多少帧,花费了多少时间。如果算出来每帧的时间大于一个值,就会记录下来。
如果收集Activity信息?
package com.argusapm.android.core.job.activity;
public class InstrumentationHooker {
private static boolean isHookSucceed = false;//是否已经hook成功
public static void doHook() {
try {
hookInstrumentation();
isHookSucceed = true;
} catch (Exception e) {
if (DEBUG) {
LogX.e(TAG, "InstrumentationHooker", e.toString());
}
}
}
static boolean isHookSucceed() {
return isHookSucceed;
}
private static void hookInstrumentation() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
Class<?> c = Class.forName("android.app.ActivityThread");
Method currentActivityThread = c.getDeclaredMethod("currentActivityThread");
boolean acc = currentActivityThread.isAccessible();
if (!acc) {
currentActivityThread.setAccessible(true);
}
Object o = currentActivityThread.invoke(null);
if (!acc) {
currentActivityThread.setAccessible(acc);
}
Field f = c.getDeclaredField("mInstrumentation");
acc = f.isAccessible();
if (!acc) {
f.setAccessible(true);
}
Instrumentation currentInstrumentation = (Instrumentation) f.get(o);
Instrumentation ins = new ApmInstrumentation(currentInstrumentation);
f.set(o, ins);
if (!acc) {
f.setAccessible(acc);
}
}
}
会去hook 当前currentActivityThread 的 mInstrumentation。 每次执行activity 的任何生命周期,都会先调用到Instrumentation 里面。把Instrumentation 设置成自己的Instrumentation,把原来的Instrumentation 保存起来。相当于代理了原来的Instrumentation。
package com.argusapm.android.core.job.activity;
/**
* 对activity启动各个阶段的消耗的时间进行监控
*
* @author ArgusAPM Team
*/
public class ApmInstrumentation extends Instrumentation {
private static final String SUB_TAG = "traceactivity";
private Instrumentation mOldInstrumentation = null;
public ApmInstrumentation(Instrumentation oldInstrumentation) {
if (oldInstrumentation instanceof Instrumentation) {
mOldInstrumentation = oldInstrumentation;
}
}
@Override
public void callApplicationOnCreate(Application app) {
ActivityCore.appAttachTime = System.currentTimeMillis();
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "callApplicationOnCreate time" + ActivityCore.appAttachTime);
}
if (mOldInstrumentation != null) {
mOldInstrumentation.callApplicationOnCreate(app);
} else {
super.callApplicationOnCreate(app);
}
}
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
if (!isActivityTaskRunning()) {
if (mOldInstrumentation != null) {
mOldInstrumentation.callActivityOnCreate(activity, icicle);
} else {
super.callActivityOnCreate(activity, icicle);
}
return;
}
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "callActivityOnCreate");
}
long startTime = System.currentTimeMillis();
if (mOldInstrumentation != null) {
mOldInstrumentation.callActivityOnCreate(activity, icicle);
} else {
super.callActivityOnCreate(activity, icicle);
}
ActivityCore.startType = ActivityCore.isFirst ? ActivityInfo.COLD_START : ActivityInfo.HOT_START;
ActivityCore.onCreateInfo(activity, startTime);
}
}
这样,就统计了你的activity 每个onXXX 方法执行的用时,而没有嵌入任何代码。
如果收集内存信息?
package com.argusapm.android.core.job.memory;
public class MemoryTask extends BaseTask {
private static final String SUB_TAG = "MemoryTask";
//定时任务
private Runnable runnable = new Runnable() {
@Override
public void run() {
if ((!isCanWork()) || (!checkTime())) {
return;
}
MemoryInfo memoryInfo = getMemoryInfo();
save(memoryInfo);
updateLastTime();
}
}
};
/**
* 获取当前内存信息
*/
private MemoryInfo getMemoryInfo() {
// 注意:这里是耗时和耗CPU的操作,一定要谨慎调用
Debug.MemoryInfo info = new Debug.MemoryInfo();
Debug.getMemoryInfo(info);
if (DEBUG) {
LogX.d(TAG, SUB_TAG,
"当前进程:" + ProcessUtils.getCurrentProcessName()
+ ",内存getTotalPss:" + info.getTotalPss()
+ " nativeSize:" + info.nativePss
+ " dalvikPss:" + info.dalvikPss
+ " otherPss:" + info.otherPss
);
}
return new MemoryInfo(ProcessUtils.getCurrentProcessName(), info.getTotalPss(), info.dalvikPss, info.nativePss, info.otherPss);
}
@Override
public void start() {
super.start();
AsyncThreadTask.executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.getMemoryDelayTime() + (int) (Math.round(Math.random() * 1000)));
}
}
关键是getMemoryInfo
这个方法。其他的定时去调用getMemoryInfo
获取内存信息,都是没有什么的。
开源框架学习总结:
1.学到一些新的思路。
2.学到一些新的设计模式。
3.扩展知识面
4.学以致用,自己以后有其他的想法,会这个技术,就可以写开源框架。