360Apm源码解析

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.学以致用,自己以后有其他的想法,会这个技术,就可以写开源框架。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值