APM监控 · 入门篇 · Android端测监控平台建设(2)

本文涉及的产品
应用实时监控服务-应用监控,每月50GB免费额度
应用实时监控服务-用户体验监控,每月100OCU免费额度
简介: 微信Matrix框架解析整体分析

微信Matrix框架解析

整体分析

  Matrix.Builder builder = new Matrix.Builder(application); // build matrix
  builder.patchListener(new TestPluginListener(this)); // add general pluginListener
  DynamicConfigImplDemo dynamicConfig = new DynamicConfigImplDemo(); // dynamic config
  // init plugin 
  IOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder()
                    .dynamicConfig(dynamicConfig)
                    .build());
  //add to matrix               
  builder.plugin(ioCanaryPlugin);
  //init matrix
  Matrix.init(builder.build());
  // start plugin 
  ioCanaryPlugin.start();

整理的结构如下:

1681524248090.png

核心功能:

  • Resource Canary:
  • Activity 泄漏
  • Bitmap 冗余
  • Trace Canary
  • 界面流畅
  • 启动耗时
  • 页面切换耗时
  • 慢函数
  • 卡顿
  • SQLite Lint: 按官方最佳实践自动化检测 SQLite 语句的使用质量
  • IO Canary: 检测文件 IO 问题
  • 文件 IO 监控
  • Closeable Leak 监控
  • 整体架构分析:

matrix-android-lib

  • Plugin 被定义为某种监控能力的抽象
  • Issue 发生的某种被监控事件
  • Report 一种观察者模式的实现,用于发现Issue时,通知观察者
  • Matrix 单例模式的实现,暴漏给外部的接口
  • matrix-config.xml 相关监控的配置项
  • IDynamicConfig 开放给用户自定义的相关监控的配置项,其实例会被各个Plugin持
  • plugin核心接口
  • IPlugin 一种监控能力的抽象
  • PluginListener 开放给用户的,感知监控模块初始化、启动、停止、发现问题等生命周期的能力。 用户可自行实现,并注入到Matrix中
  • Plugin 1、对IPlugin的默认实现,以触发相关PluginListener生命周期 2、实现 IssuePublisher.OnIssueDetectListener接口的onDetectIssue方法
  • 该方法会在 具体监控事件发生时,被 Matrix.with().getPluginByClass(xxx).onDetectIssue(Issue)这样调用。注意这里是第一种通知方式
  • 该方法内部对Issue相关环境变量进行赋值,譬如引发该Issue的Plugin信息
  • 该方法最终触发PluginListener#onReportIssue
public interface IPlugin {
    /**
     * 用于标识当前的监控,相当于名称索引(也可用classname直接索引)
     */
    String getTag();
    /**
     * 在Matrix对象构建时被调用
     */
    void init(Application application, PluginListener pluginListener);
    /**
     * 对activity前后台转换的感知能力
     */
    void onForeground(boolean isForeground);
    void start();
    void stop();
    void destroy();
}
public interface PluginListener {
    void onInit(Plugin plugin);
    void onStart(Plugin plugin);
    void onStop(Plugin plugin);
    void onDestroy(Plugin plugin);
    void onReportIssue(Issue issue);
}

Matrix对外接口

public class Matrix {
    private static final String TAG = "Matrix.Matrix";
  /**********************************  单例实现 **********************/
  private static volatile Matrix sInstance;
    public static Matrix init(Matrix matrix) {
        if (matrix == null) {
            throw new RuntimeException("Matrix init, Matrix should not be null.");
        }
        synchronized (Matrix.class) {
            if (sInstance == null) {
                sInstance = matrix;
            } else {
                MatrixLog.e(TAG, "Matrix instance is already set. this invoking will be ignored");
            }
        }
        return sInstance;
    }
    public static boolean isInstalled() {
        return sInstance != null;
    }
    public static Matrix with() {
        if (sInstance == null) {
            throw new RuntimeException("you must init Matrix sdk first");
        }
        return sInstance;
    }
    /****************************  构造函数 **********************/
    private final Application     application;
  private final HashSet<Plugin> plugins;
    private final PluginListener  pluginListener;
  private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
        this.application = app;
        this.pluginListener = listener;
        this.plugins = plugins;
        for (Plugin plugin : plugins) {
            plugin.init(application, pluginListener);
            pluginListener.onInit(plugin);
        }
    }
  /****************************  控制能力 **********************/
    public void startAllPlugins() {
        for (Plugin plugin : plugins) {
            plugin.start();
        }
    }
    public void stopAllPlugins() {
        for (Plugin plugin : plugins) {
            plugin.stop();
        }
    }
    public void destroyAllPlugins() {
        for (Plugin plugin : plugins) {
            plugin.destroy();
        }
    }
  /****************************  get | set **********************/
    public Plugin getPluginByTag(String tag) {
        for (Plugin plugin : plugins) {
            if (plugin.getTag().equals(tag)) {
                return plugin;
            }
        }
        return null;
    }
    public <T extends Plugin> T getPluginByClass(Class<T> pluginClass) {
        String className = pluginClass.getName();
        for (Plugin plugin : plugins) {
            if (plugin.getClass().getName().equals(className)) {
                return (T) plugin;
            }
        }
        return null;
    }
  /****************************  其他 **********************/
  public static void setLogIml(MatrixLog.MatrixLogImp imp) {
        MatrixLog.setMatrixLogImp(imp);
    }
}

Utils 辅助功能

  • MatrixLog 开放给用户的log能力
  • MatrixHandlerThread 公共线程,往往用于异步线程执行一些任务
  • DeviceUtil 获取相关设备信息
  • MatrixUtil 判断主线程等其他utils

IssuePublisher被监控事件观察者

  • Issue 被监控事件 type:类型,用于区分同一个tag不同类型的上报 tag: 该上报对应的tag stack:该上报对应的堆栈 process:该上报对应的进程名 time:issue 发生的时间

IssuePublisher 观察者模式

  • 持有一个 发布Listener(其实现往往是上文的 Plugin)
  • 持有一个 已发布信息的Map,在一次运行时长内,避免针对同一事件的重复发布
  • 一般而言,某种监控的监控探测器往往继承该类,并在检测到事件发生时,调用publishIssue(Issue)—>IssuePublisher.OnIssueDetectListener接口的onDetectIssue方法—>最终触发PluginListener#onReportIssue

Matrix的模块IO监测核心源码分析

IO Canary:核心的作用是检测文件 IO 问题,包括:文件 IO 监控和 Closeable Leak 监控。要想理解IO的监测和看懂开源的代码,最重要的基础就是掌握Native和Java层面的Hook。

Java层面的hook主要是基于反射技术,大家都比较熟悉了,那我们来聊一聊Native层面的Hook。在JVM层面,Android使用Android PLT (Procedure Linkage Table)Hook和Inline Hookptrace三种主流的技术。

Matrix采用PLT的技术来实现SO文件API的Hook。

ELF: en.wikipedia.org/wiki/Execut…

refspecs.linuxbase.org/elf/elf.pdf

ELF文件的三种形式:

  • 可重定位的对象文件(Relocatable file),也就是我们熟悉的那些.o文件,当然.a库也算(因为它是.o的集合)
  • 可执行的对象文件(Executable file),linux下的可执行文件
  • 可被共享的对象文件(Shared object file),动态链接库.so

我们思考下C的程序是需要进行编译和链接再到最后的运行的。那么ELF文件从这个参与程序运行的角度也是分为2种视图的。

1681525067555.png

ELF header 位于文件的最开始处,描述整个文件的组织结构。Program Header Table 告诉系统如何创建进程镜像,在执行程序时必须存在,在 relocatable files 中则不需要。每个 program header 分别描述一个 segment,包括 segment 在文件和内存中的大小及地址等等。执行视图中的 segment 其实是由很多个 section 组成。在一个进程镜像中通常具有 text segment 和 data segment 等等。

关于重定位的作用和概念

重定位就是把符号引用与符号定义链接起来的过程,这也是 android linker 的主要工作之一。当程序中调用一个函数时,相关的 call 指令必须在执行期将控制流转到正确的目标地址。所以,so 文件中必须包含一些重定位相关的信息,linker 据此完成重定位的工作。

docs.oracle.com/cd/E19683-0…

android.googlesource.com/platform/bi…

1681525146163.png

1681525178054.png

符号表表项的结构为elf32_sym:

typedef struct elf32_sym {
    Elf32_Word  st_name;    /* 名称 – index into string table */
    Elf32_Addr  st_value;   /* 偏移地址 */
    Elf32_Word  st_size;    /* 符号长度( 例如,函数的长度) */
    unsigned char   st_info;    /* 类型和绑定类型 */
    unsigned char   st_other;   /* 未定义 */
    Elf32_Half  st_shndx;   /* section header的索引号,表示位于哪个section中 */
} Elf32_Sym;

重定位核心代码:

androidxref.com/8.0.0_r4/xr… (具体的重定位类型定义和计算方法可以参考elf说明文档的 4.6.1.2 小节)

1681525236911.png

Android PLT Hook的基本原理

Linux在执行动态链接的ELF的时候,为了优化性能使用了一个叫延时绑定的策略。当在动态链接的ELF程序里调用共享库的函数时,第一次调用时先去查找PLT表中相应的项目,而PLT表中再跳跃到GOT表中希望得到该函数的实际地址,但这时GOT表中指向的是PLT中那条跳跃指令下面的代码,最终会执行_dl_runtime_resolve()并执行目标函数。因此,PLT Hook通过直接修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。

1681525288022.png

IO 监控流程:

1681525355383.png

JNIEXPORT jboolean JNICALL
        Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
            __android_log_print(ANDROID_LOG_INFO, kTag, "doHook");
            for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {
                const char* so_name = TARGET_MODULES[i];
                __android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name);
                loaded_soinfo* soinfo = elfhook_open(so_name);
                if (!soinfo) {
                    __android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name);
                    continue;
                }
                //hook OS
                elfhook_replace(soinfo, "open", (void*)ProxyOpen, (void**)&original_open);
                elfhook_replace(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64);
                bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);
                if (is_libjavacore) {
                    if (!elfhook_replace(soinfo, "read", (void*)ProxyRead, (void**)&original_read)) {
                        __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk");
                        if (!elfhook_replace(soinfo, "__read_chk", (void*)ProxyRead, (void**)&original_read)) {
                            __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk");
                            elfhook_close(soinfo);
                            return false;
                        }
                    }
                    //hook OS
                    if (!elfhook_replace(soinfo, "write", (void*)ProxyWrite, (void**)&original_write)) {
                        __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk");
                        if (!elfhook_replace(soinfo, "__write_chk", (void*)ProxyWrite, (void**)&original_write)) {
                            __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk");
                            elfhook_close(soinfo);
                            return false;
                        }
                    }
                }
                //hook OS
                elfhook_replace(soinfo, "close", (void*)ProxyClose, (void**)&original_close);
                elfhook_close(soinfo);
            }
            return true;
        }

hook的替换核心代码:(本质上就是置指针替换)

1681525423963.png

看看代理方法:

1681525461632.png

很明显腾讯的人没有考虑到自线程的问题。这里可以优化的。具体的其他部分的细节请参见源码。

Matrix的模块内存泄漏监测核心源码分析

github.com/Tencent/mat…

设计目的:

  • 自动且较为准确地监测Activity泄漏,发现泄漏之后再触发Dump Hprof而不是根据预先设定的内存占用阈值盲目触发
  • 自动获取泄漏的Activity和冗余Bitmap对象的引用链
  • 能灵活地扩展Hprof的分析逻辑,必要时允许提取Hprof文件人工分析

为了解决线上监测和后台分析,Matrix的ResourceCanary最终决定将监测步骤和分析步骤拆成两个独立的工具,以满足设计目标。

  • Hprof文件留在了服务端,为人工分析提供了机会
  • 如果跳过触发Dump Hprof,甚至可以把监测步骤在现网环境启用,以发现测试阶段难以触发的Activity泄漏

客户端解决的问题是内存泄漏的监测和Hprof文件的裁剪,具体看下面的流程图:

1681525570460.pngResourcePlugin

ResourcePlugin是该模块的入口,负责注册Android生命周期的监听以及配置部分参数和接口回调。

ActivityRefWatcherActivityRefWatcher负责的任务有弹出Dump内存的Dialog、Dump内存数据、读取内存数据裁剪Hprof文件、生成包含裁剪后的Hprof以及泄漏的Activity的信息(进程号、Activity名、时间等)、通知主线程完成内存信息的备份并关闭Dialog。

我们看下最为核心的内存泄漏监测代码:

//ActivityRefWatcher
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {
        private int mAppStatusCounter = 0;
        private int mUIConfigChangeCounter = 0;
        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            mCurrentCreatedActivityCount.incrementAndGet();
        }
        @Override
        public void onActivityStarted(Activity activity) {
            if (mAppStatusCounter <= 0) {
                MatrixLog.i(TAG, "we are in foreground, start watcher task.");
                mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
            }
            if (mUIConfigChangeCounter < 0) {
                ++mUIConfigChangeCounter;
            } else {
                ++mAppStatusCounter;
            }
        }
        @Override
        public void onActivityStopped(Activity activity) {
            if (activity.isChangingConfigurations()) {
                --mUIConfigChangeCounter;
            } else {
                --mAppStatusCounter;
                if (mAppStatusCounter <= 0) {
                    MatrixLog.i(TAG, "we are in background, stop watcher task.");
                    mDetectExecutor.clearTasks();
                }
            }
        }
        @Override
        public void onActivityDestroyed(Activity activity) {
          //当activity销毁的时候开始。。。
            pushDestroyedActivityInfo(activity);
            synchronized (mDestroyedActivityInfos) {
                mDestroyedActivityInfos.notifyAll();
            }
        }
    };
 private void pushDestroyedActivityInfo(Activity activity) {
        final String activityName = activity.getClass().getName();
        //该Activity确认存在泄漏,且已经上报
        if (isPublished(activityName)) {
            MatrixLog.d(TAG, "activity leak with name %s had published, just ignore", activityName);
            return;
        }
        final UUID uuid = UUID.randomUUID();
        final StringBuilder keyBuilder = new StringBuilder();
        //生成Activity实例的唯一标识
        keyBuilder.append(ACTIVITY_REFKEY_PREFIX).append(activityName)
            .append('_').append(Long.toHexString(uuid.getMostSignificantBits())).append(Long.toHexString(uuid.getLeastSignificantBits()));
        final String key = keyBuilder.toString();
        //构造一个数据结构,表示一个已被destroy的Activity
        final DestroyedActivityInfo destroyedActivityInfo
            = new DestroyedActivityInfo(key, activity, activityName, mCurrentCreatedActivityCount.get());
       //放入ConcurrentLinkedQueue数据结构中,用于后续的检查
        mDestroyedActivityInfos.add(destroyedActivityInfo);
    }

内存泄漏的核心代码:

private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {
        @Override
        public Status execute() {
            // If destroyed activity list is empty, just wait to save power.
            while (mDestroyedActivityInfos.isEmpty()) {
                synchronized (mDestroyedActivityInfos) {
                    try {
                        mDestroyedActivityInfos.wait();
                    } catch (Throwable ignored) {
                        // Ignored.
                    }
                }
            }
            // Fake leaks will be generated when debugger is attached.
           //Debug调试模式,检测可能失效,直接return
            if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) {
                MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed.");
                return Status.RETRY;
            }
             //创建一个对象的弱引用
            final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
            triggerGc();
            //系统未执行GC,直接return
            if (sentinelRef.get() != null) {
                // System ignored our gc request, we will retry later.
                MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");
                return Status.RETRY;
            }
            final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
            while (infoIt.hasNext()) {
                final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
               //该实例对应的Activity已被标泄漏,跳过该实例
                if (isPublished(destroyedActivityInfo.mActivityName)) {
                    MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
                    infoIt.remove();
                    continue;
                }
               //若不能通过弱引用获取到Activity实例,表示已被回收,跳过该实例
                if (destroyedActivityInfo.mActivityRef.get() == null) {
                    // The activity was recycled by a gc triggered outside.
                    MatrixLog.v(TAG, "activity with key [%s] was already recycled.", destroyedActivityInfo.mKey);
                    infoIt.remove();
                    continue;
                }
                 //该Activity实例 检测到泄漏的次数+1
                ++destroyedActivityInfo.mDetectedCount;
    //当前显示的Activity实例与泄漏的Activity实例相差几个Activity跳转
                long createdActivityCountFromDestroy = mCurrentCreatedActivityCount.get() - destroyedActivityInfo.mLastCreatedActivityCount;
               //若Activity实例 检测到泄漏的次数未达到阈值,或者泄漏的Activity与当前显示的Activity很靠近,可认为是一种容错手段(实际应用中有这种场景),跳过该实例
                if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
                    || (createdActivityCountFromDestroy < CREATED_ACTIVITY_COUNT_THRESHOLD && !mResourcePlugin.getConfig().getDetectDebugger())) {
                    // Although the sentinel tell us the activity should have been recycled,
                    // system may still ignore it, so try again until we reach max retry times.
                    MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n"
                            + "exists in %s times detection with %s created activities during destroy, wait for next detection to confirm.",
                        destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount, createdActivityCountFromDestroy);
                    continue;
                }
                MatrixLog.i(TAG, "activity with key [%s] was suspected to be a leaked instance.", destroyedActivityInfo.mKey);
                if (mHeapDumper != null) {
                    final File hprofFile = mHeapDumper.dumpHeap();
                    if (hprofFile != null) {
                        markPublished(destroyedActivityInfo.mActivityName);
                        final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
                        mHeapDumpHandler.process(heapDump);
                        infoIt.remove();
                    } else {
                        MatrixLog.i(TAG, "heap dump for further analyzing activity with key [%s] was failed, just ignore.",
                                destroyedActivityInfo.mKey);
                        infoIt.remove();
                    }
                } else {
                    // Lightweight mode, just report leaked activity name.
                    MatrixLog.i(TAG, "lightweight mode, just report leaked activity name.");
                    markPublished(destroyedActivityInfo.mActivityName);
                    if (mResourcePlugin != null) {
                        final JSONObject resultJson = new JSONObject();
                        try {
                            resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
                        } catch (JSONException e) {
                            MatrixLog.printErrStackTrace(TAG, e, "unexpected exception.");
                        }
                        mResourcePlugin.onDetectIssue(new Issue(resultJson));
                    }
                }
            }
            return Status.RETRY;
        }
    };

RetryableTaskExecutor

RetryableTaskExecutor中包含了两个Handler对象,一个mBackgroundHandlermMainHandler,分别给主线程和后台的线程提交任务。默认重试次数是3。

AndroidHeapDumper

AndroidHeapDumper这个其实就是封装了android.os.Debug的接口的类。主要是用系统提供的类android.os.DebugDump内存信息到本地,android.os.Debug会在本地生成一个Hprof文件,也是Matrix需要分析和裁剪的原始文件。

注意:一般Dump一次要5s~15s之间,线上建议不要使用,有一定的风险。

Dump的时候,AndroidHeapDumper会展示一个Dialog提示当前正在Dump中,Dump完毕就会将Dialog关闭。

 Debug.dumpHprofData(hprofFile.getAbsolutePath());

Matrix的模块Trace模块核心源码分析

Trace Canary: 用于监控界面流畅性、启动耗时、页面切换耗时、慢函数及卡顿等问题。(思考一下,技术上怎么实现)

入口函数探针分析:

public class TracePlugin extends Plugin {
    private static final String TAG = "Matrix.TracePlugin";
    private final TraceConfig traceConfig;
    private EvilMethodTracer evilMethodTracer;//慢函数
    private StartupTracer startupTracer; //启动监测
    private FrameTracer frameTracer; //fps
    private AnrTracer anrTracer; //anr
    public TracePlugin(TraceConfig config) {
        this.traceConfig = config;
    }
  ...

【关键知识点1】: MessageQueue中的IdleHandler接口有什么用?

在Android中,我们可以处理Message,这个Message我们可以立即执行也可以delay 一定时间执行。Handler线程在执行完所有的Message消息,它会wait,进行阻塞,直到有新的Message到达。如果这样子,那么这个线程也太浪费了。MessageQueue提供了另一类消息,IdleHandler。也就是说当我们的MessageQueue中的消息被处理完后,就会触发一次或者多次回调消息。

应用场景:1、比如主线程在开始加载页面完成后,如果线程空闲就提前加载些二级页面的内容。

                   2、消息触发器 例如在APM中的作用

                   3、优化Activity的启动时间,在Resume中是不是可以增加idle的监听

Looper.myQueue().addIdleHandler(() -> {
            initializeData();
        return false;
        });

源码分析:

  Message next(){
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }
                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                //根据IdleHandler中的回掉方法来判断是否移除
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

LooperMonitor类监测卡顿问题:

发生在Android主线程的每16ms重绘操作依赖于Main Looper中消息的发送和获取。如果App一切运行正常,无卡顿无丢帧现象发生,那么开发者的代码在主线程Looper消息队列中发送和接收消息的时间会很短,理想情况是16ms,这是也是Android系统规定的时间。但是,如果一些发生在主线程的代码写的太重,执行任务花费时间太久,就会在主线程延迟Main Looper的消息在16ms尺度范围内的读和写。

我们如何检测卡顿的问题?

使用主线程的Looper监测系统发生的卡顿和丢帧。编程技巧是设置一个阈值,看是否可以打印stack信息。

网络上说使用Android的Choreographer监测App发生的UI卡顿丢帧问题,本质上还是利用了Android的主线程的Looper消息机制。Android 系统每隔 16.67 ms 都会发送一个 VSYNC 信号触发 UI 的渲染,正常情况下两个 VSYNC 信号之间是 16.67 ms ,如果超过 16.67 ms 则可以认为渲染发生了卡顿。

Choreographer.getInstance()
            .postFrameCallback(new Choreographer.FrameCallback() {
                @Override
                public void doFrame(long l) {
                     if(frameTimeNanos - mLastFrameNanos > 100) {
                        ...
                     }
                     mLastFrameNanos = frameTimeNanos;
                     Choreographer.getInstance().postFrameCallback(this);
                }
        });

本质:判断相邻的两次 FrameCallback.doFrame(long l) 间隔是否超过阈值,如果超过阈值则发生了卡顿,则可以在另外一个子线程中 dump 当前主线程的堆栈信息进行分析。

消息处理

UIThreadMonitor

init():

public void init(TraceConfig config) {
        if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
            throw new AssertionError("must be init in main thread!");
        }
        this.isInit = true;
        this.config = config;
        choreographer = Choreographer.getInstance();
        callbackQueueLock = reflectObject(choreographer, "mLock");              
        callbackQueues = reflectObject(choreographer, "mCallbackQueues");                                                                    // 代码 1
        addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);             // 代码 2
        addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);     // 代码 3
        addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);     // 代码 4  
        frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");
        LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {                                                                  // 代码 5  
            @Override
            public boolean isValid() {
                return isAlive;
            }
            @Override
            public void dispatchStart() {
                super.dispatchStart();
                UIThreadMonitor.this.dispatchBegin();                                                                                       // 代码 6
            }
            @Override
            public void dispatchEnd() {
                super.dispatchEnd();
                UIThreadMonitor.this.dispatchEnd();                                                                                      // 代码 7
            }
        });
                ......
    }  

代码 1:

  • 通过反射拿到了 Choreographer 实例的 mCallbackQueues 属性,mCallbackQueues 是一个回调队列数组 CallbackQueue[] mCallbackQueues,其中包括四个回调队列,
  • 第一个是输入事件回调队列 CALLBACK_INPUT = 0,
  • 第二个是动画回调队列 CALLBACK_ANIMATION = 1,
  • 第三个是遍历绘制回调队列 CALLBACK_TRAVERSAL = 2,
  • 第四个是提交回调队列 CALLBACK_COMMIT = 3。 这四个阶段在每一帧的 UI 渲染中是依次执行的,每一帧中各个阶段开始时都会回调 mCallbackQueues 中对应的回调队列的回调方法。


代码 2:

  • 通过反射拿到输入事件回调队列的 addCallbackLocked 方法


代码 3:

  • 通过反射拿到动画回调队列的 addCallbackLocked 方法


代码 4:

  • 通过反射拿到遍历绘制回调队列的addCallbackLocked 方法


代码 5:

  • 通过 LooperMonitor.register(LooperDispatchListener listener) 方法向 LooperMonitor 中设置 LooperDispatchListener listener


  • 代码 6:
  • 在 Looper.loop() 中的消息处理开始时的回调
  • 代码 7:
  • 在 Looper.loop() 中的消息处理结束时的回调

核心:

private void dispatchBegin() {
  //记录2个时间 线程起始时间 和CPU的开始时间
        token = dispatchTimeMs[0] = SystemClock.uptimeMillis();
        dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis();
        AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);
        synchronized (observers) {
            for (LooperObserver observer : observers) {
                if (!observer.isDispatchBegin()) {
                    observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token);
                }
            }
        }
    }  
private void dispatchEnd() {                                                                                            // 代码 3
        if (isBelongFrame) {
            doFrameEnd(token);
        }
        dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();
        dispatchTimeMs[1] = SystemClock.uptimeMillis();
        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, isBelongFrame);
                }
            }
        }
    }  

核心点总结:

  • 在 UIThreadMonitor 中有两个长度是三的数组 queueStatusqueueCost 分别对应着每一帧中输入事件阶段、动画阶段、遍历绘制阶段的状态和耗时,queueStatus 有三个值:DO_QUEUE_DEFAULT、DO_QUEUE_BEGIN 和 DO_QUEUE_END。
  • UIThreadMonitor 实现 Runnable 接口,也是为了将 UIThreadMonitor 作为输入事件回调 CALLBACK_INPUT 的回调方法,设置到 Choreographer 中去的。

看到这里应该搞明白了卡顿的检测原理,那么FPS的计算呢?

每一帧的时间信息通过 HashSet observers 回调出去,看一下是在哪里向 observers 添加 LooperObserver 回调的。主要看一下 FrameTracer 这个类,其中涉及到了帧率 FPS 的计算相关的代码。

FPSCollectorFrameTracer 的一个内部类,实现了 IDoFrameListener 接口,主要逻辑是在 doFrameAsync() 方法中

  • 代码 1:会根据当前 ActivityName 创建一个对应的 FrameCollectItem 对象,存放在 HashMap 中
  • 代码 2:调用 FrameCollectItem#collect(),计算帧率 FPS 等一些信息
  • 代码 3:如果此 Activity 的绘制总时间超过 timeSliceMs(默认是 10s),则调用 FrameCollectItem#report() 上报统计数据,并从 HashMap 中移除当前 ActivityName 和对应的 FrameCollectItem 对象
private class FPSCollector extends IDoFrameListener {
        private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper());
        private HashMap<String, FrameCollectItem> map = new HashMap<>();
        @Override
        public Handler getHandler() {
            return frameHandler;
        }
        @Override
        public void doFrameAsync(String focusedActivityName, long frameCost, int droppedFrames) {
            super.doFrameAsync(focusedActivityName, frameCost, droppedFrames);
            if (Utils.isEmpty(focusedActivityName)) {
                return;
            }
            FrameCollectItem item = map.get(focusedActivityName);       // 代码 1
            if (null == item) {
                item = new FrameCollectItem(focusedActivityName);
                map.put(focusedActivityName, item);
            }
            item.collect(droppedFrames);                                                                // 代码 2
            if (item.sumFrameCost >= timeSliceMs) { // report                       // 代码 3
                map.remove(focusedActivityName);
                item.report();
            }
        }
    }

FrameCollectItem:计算FPS。

  • 根据 float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost) 计算 fps 值
  • sumDroppedFrames 统计总的掉帧个数
  • sumFrameCost 代表总耗时

计算帧率参考资料:

github.com/friendlyrob… 熟读代码 并应用到自己的项目中。

关于hook的部分:

ActivityThreadHacker.java

利用了反射机制进行了hook,代码比较清晰,目的很明确就是利用自己写的HackCallback来替换ActivityThread类里的mCallback,达到偷梁换柱的效果,这样做的意义就是可以拦截mCallback的原有的消息,然后选择自己要处理的消息。搞清楚Activity的启动流程 AMS和ActivityThread进程之间的交互。

1681526173561.png

获取到Activity的启动时机。

AppMethodBeat 通过hook记录每个方法执行的时间。

/**
     * hook method when it's called in.
     *
     * @param methodId
     */
    public static void i(int methodId) {
        if (status <= STATUS_STOPPED) {
            return;
        }
        if (methodId >= METHOD_ID_MAX) {
            return;
        }
        if (status == STATUS_DEFAULT) {
            synchronized (statusLock) {
                if (status == STATUS_DEFAULT) {
                    realExecute();
                    status = STATUS_READY;
                }
            }
        }
        if (Thread.currentThread().getId() == sMainThread.getId()) {
            if (assertIn) {
                android.util.Log.e(TAG, "ERROR!!! AppMethodBeat.i Recursive calls!!!");
                return;
            }
            assertIn = true;
            if (sIndex < Constants.BUFFER_SIZE) {
                mergeData(methodId, sIndex, true);
            } else {
                sIndex = -1;
            }
            ++sIndex;
            assertIn = false;
        }
    }
    /**
     * hook method when it's called out.
     *
     * @param methodId
     */
    public static void o(int methodId) {
        if (status <= STATUS_STOPPED) {
            return;
        }
        if (methodId >= METHOD_ID_MAX) {
            return;
        }
        if (Thread.currentThread().getId() == sMainThread.getId()) {
            if (sIndex < Constants.BUFFER_SIZE) {
                mergeData(methodId, sIndex, false);
            } else {
                sIndex = -1;
            }
            ++sIndex;
        }
    }
 /**
     * merge trace info as a long data
     *
     * @param methodId
     * @param index
     * @param isIn
     */
    private static void mergeData(int methodId, int index, boolean isIn) {
        if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
          //记录上面2个方法的时间差
            sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
        }
        long trueId = 0L;
        if (isIn) {
            trueId |= 1L << 63;
        }
        trueId |= (long) methodId << 43;
        trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
        sBuffer[index] = trueId;
        checkPileup(index);
        sLastIndex = index;
    }

源码读到这里是不是可以想一想,我们应该怎么找方法的调用?

github.com/Tencent/mat…

利用ASM技术完成在方法前执行“com/tencent/matrix/trace/core/AppMethodBeat”这个class里的i()方法,在每个方法最后执行o()方法。

ASM

应用场景:

  • 无痕埋点
  • hook
  • apm监控
  • 编译期间动态修改代码需求
  • ASM是什么

ASM 可以直接产生二进制的class 文件,也可以在增强既有类的功能。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。

blog.jamesdbloom.com/JavaCodeToB…

blog.csdn.net/weelyy/arti…

asm.ow2.io/

1681526274892.png

1681526357448.png

class 文件结构:

  • Magic:该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
  • **Version:**该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
  • **Constant Pool:**该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
  • Access_flag: 该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。ACC_
  • This Class: 指向表示该类全限定名称的字符串常量的指针。
  • Super Class: 指向表示父类全限定名称的字符串常量的指针。
  • Interfaces: 一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改:将类名称改为子类名称;将父类改为派生前的类名称;如果有必要,增加新的实现接口。
  • Fields: 该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
  • Methods: 该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。
  • Class attributes: 该项存放了在该文件中类或接口所定义的属性的基本信息。

核心类

  • ClassReader:该类用来解析编译过的class字节码文件。(事件生产者)
  • ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。(事件消费者,是ClassVisitor的子类)
  • ClassVisitor:主要负责 “拜访” 类成员信息。其中包括标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块。
  • AdviceAdapter:实现了MethodVisitor接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。

函数插桩

什么是函数插桩?

插桩:目标程序代码中某些位置插入或修改成一些代码,从而在目标程序运行过程中获取某些程序状态并加以分析。简单来说就是在代码中插入代码。 那么函数插桩,便是在函数中插入或修改代码。在Android编译过程中,往字节码里插入自定义的字节码,所以也可以称为字节码插桩

作用

函数插桩可以帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。 应用到在Android中,可以用来做用行为统计、方法耗时统计等功能。

About Gradle Transform API

google.github.io/android-gra…

Transform的使用场景:

一般我们使用Transform会有下面两种场景

  1. 我们需要对编译class文件做自定义的处理。
  2. 我们需要读取编译产生的class文件,做一些其他事情,但是不需要修改它。
public abstract class Transform {
    public abstract String getName(); //自定义的Transform的名字
    public abstract Set<ContentType> getInputTypes(); //Transform处理的输入类型 (CLASSES、RESOURCES)
    public abstract Set<? super Scope> getScopes();
    public abstract boolean isIncremental(); // 是否支持增量编译
}

Scope代码:

enum Scope implements ScopeType {
        /** Only the project content */
        PROJECT(0x01), //只是当前工程的代码
        /** Only the project's local dependencies (local jars) */
        PROJECT_LOCAL_DEPS(0x02), // 工程的本地jar
        /** Only the sub-projects. */
        SUB_PROJECTS(0x04),  // 只包含子工工程
        /** Only the sub-projects's local dependencies (local jars). */
        SUB_PROJECTS_LOCAL_DEPS(0x08),
        /** Only the external libraries */
        EXTERNAL_LIBRARIES(0x10),
        /** Code that is being tested by the current variant, including dependencies */
        TESTED_CODE(0x20),
        /** Local or remote dependencies that are provided-only */
        PROVIDED_ONLY(0x40);
    }

测试代码:

public void transform(TransformInvocation invocation) {
        for (TransformInput input : invocation.getInputs()) {
            input.getJarInputs().parallelStream().forEach(jarInput -> {
            File src = jarInput.getFile();
            JarFile jarFile = new JarFile(file);
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                //处理
            }
        }
    }


相关实践学习
通过云拨测对指定服务器进行Ping/DNS监测
本实验将通过云拨测对指定服务器进行Ping/DNS监测,评估网站服务质量和用户体验。
相关文章
|
1月前
|
移动开发 监控 Android开发
Android & iOS 使用 ARMS 用户体验监控(RUM)的最佳实践
本文主要介绍了 ARMS 用户体验监控的基本功能特性,并介绍了在几种常见场景下的最佳实践。
|
1月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
112 1
|
2月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
28天前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
28 1
|
1月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
85 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
23天前
|
XML IDE Java
安卓应用开发入门:从零开始的旅程
【10月更文挑战第23天】本文将带领读者开启一段安卓应用开发的奇妙之旅。我们将从最基础的概念讲起,逐步深入到开发实践,最后通过一个简易的代码示例,展示如何将理论知识转化为实际的应用。无论你是编程新手,还是希望扩展技能的软件工程师,这篇文章都将为你提供有价值的指导和启发。
28 0
|
1月前
|
开发框架 移动开发 Android开发
安卓与iOS开发中的跨平台解决方案:Flutter入门
【9月更文挑战第30天】在移动应用开发的广阔舞台上,安卓和iOS两大操作系统各自占据半壁江山。开发者们常常面临着选择:是专注于单一平台深耕细作,还是寻找一种能够横跨两大系统的开发方案?Flutter,作为一种新兴的跨平台UI工具包,正以其现代、响应式的特点赢得开发者的青睐。本文将带你一探究竟,从Flutter的基础概念到实战应用,深入浅出地介绍这一技术的魅力所在。
81 7
|
2月前
|
监控 安全 Java
Kotlin 在公司上网监控中的安卓开发应用
在数字化办公环境中,公司对员工上网行为的监控日益重要。Kotlin 作为一种基于 JVM 的编程语言,具备简洁、安全、高效的特性,已成为安卓开发的首选语言之一。通过网络请求拦截,Kotlin 可实现网址监控、访问时间记录等功能,满足公司上网监控需求。其简洁性有助于快速构建强大的监控应用,并便于后续维护与扩展。因此,Kotlin 在安卓上网监控应用开发中展现出广阔前景。
20 1
|
2月前
|
监控 Android开发 iOS开发
深入探索安卓与iOS的系统架构差异:理解两大移动平台的技术根基在移动技术日新月异的今天,安卓和iOS作为市场上最为流行的两个操作系统,各自拥有独特的技术特性和庞大的用户基础。本文将深入探讨这两个平台的系统架构差异,揭示它们如何支撑起各自的生态系统,并影响着全球数亿用户的使用体验。
本文通过对比分析安卓和iOS的系统架构,揭示了这两个平台在设计理念、安全性、用户体验和技术生态上的根本区别。不同于常规的技术综述,本文以深入浅出的方式,带领读者理解这些差异是如何影响应用开发、用户选择和市场趋势的。通过梳理历史脉络和未来展望,本文旨在为开发者、用户以及行业分析师提供有价值的见解,帮助大家更好地把握移动技术发展的脉络。
93 6
|
2月前
|
Android开发 开发者
安卓开发中的自定义视图:从入门到精通
【9月更文挑战第19天】在安卓开发的广阔天地中,自定义视图是一块充满魔力的土地。它不仅仅是代码的堆砌,更是艺术与科技的完美结合。通过掌握自定义视图,开发者能够打破常规,创造出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战应用,一步步展示如何用代码绘出心中的蓝图。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往创意和效率的大门。让我们一起探索自定义视图的秘密,将你的应用打造成一件艺术品吧!
62 10

热门文章

最新文章

下一篇
无影云桌面