卡顿监测 · 方案篇 · Android卡顿监测指导原则(2)

简介: 卡顿监测 · 方案篇 · Android卡顿监测指导原则

六、 分析工具

工具对比

image.png

使用指南

  • 需要分析Native代码,选Simpleperf
  • 需要分析系统调用,选Systrace
  • 需要分析应用流程和耗时,选TraceView / 插桩之后的Systrace
  • 需要分析其他应用,选Nanoscope
  • 灰度环境分析Java代码,选Rhea

七、 卡顿指标

怎么用一个指标直观反映卡顿呢?

监测指标

image.png

流畅Smoothness计算

  1. 当Vsync信号到达时会会调用HAL层的HWComposer.vsync()函数,通知HWComposer引擎进行GPU渲染和显示,,然后发送Vsync信号给SurfaceFinger处理
  2. SurfaceFinger接收到Vsync信号后,调用SurfaceFlinger的addResyncSample函数用来处理 Vsync 信号,addResyncSample函数可以将App的渲染帧同步到显示器的刷新时间,以避免出现撕裂和卡顿等问题。
  3. EventThread通过onVsyncEvent函数将Vsync信号分发给需要使用Vsync信号的App,实现更平滑和流畅的渲染效果
  4. 当系统收到显示器的 Vsync 信号时,DisplayEventReceiver.onVsync() 函数会被调用,并将时间戳和Displayer物理属性传递给App
  5. App收到时间戳和Displayer物理属性后,FrameHandler可以帮助应用程序将渲染帧与 Vsync 信号同步,当 FrameHandler 接收到 Vsync 信号时,FrameHandler会调用 sendMessage() 方法,并将帧同步消息作为参数传递给该方法
  6. image.png
  7. 流畅度存在哪些痛点问题?

流畅度存在两个痛点:

第一个痛点是: 数据量大,不方便统计,导致淹没真实卡顿Case。

第二个痛点是: 不能简单使用平均值和方差。因为不同设备的卡顿标准线不一样,我们应该按照设备等级划分标注线

流畅度指标是怎样衡量的?

  • 指标一: 流畅度评分
  • 压缩数据
  • 加权放大卡顿
  • image.png
  • 指标二: XPM评分(denzelzhou):离散程度
  • 帧绘制时长到标准绘制时长的距离(点到线的距离)
  • 距离标准绘制时长越远就越卡顿
  • image.png

八、监测SOP

监测范围

慢函数监测

技术需求: 通过外部配置阈值记录Android慢函数,如果超过阈值,那么将慢函数方法名和耗时间信息记录在本地JSON文件

思路:

  1. 首先定义一个类 SlowFunctionClassVisitor,继承自 ClassVisitor,用于实现对类的字节码的修改。
  2. 在 SlowFunctionClassVisitor 中重写 visitMethod 方法,用于实现对方法的字节码的修改。在 visitMethod 方法中,先调用父类的 visitMethod 方法,然后使用 ASM 的 API 生成新的方法字节码,并将原来的方法字节码替换为新的方法字节码。
  3. 在生成新的方法字节码时,使用 Label 和 JumpInsnNode 等 ASM 的 API 插入代码,实现对方法的耗时进行判断。如果方法耗时超过阈值,则记录慢函数信息,并将其写入本地 JSON 文件中。
  4. 为了实现记录慢函数信息和将其写入本地 JSON 文件中,使用了 org.json.JSONObject 和 java.io.FileWriter 等相关的 API。

假设我们要记录的慢函数是指执行时间超过100ms的函数,并且阈值的配置方式是通过一个配置文件,其中包含一个键值对 "slow_function_threshold=100",存放在assets目录下的config.json文件中。

首先,在Android Studio中创建一个新的Android项目,并将以下代码添加到build.gradle文件的dependencies块中:

dependencies {
    implementation 'org.ow2.asm:asm:9.2'
    implementation 'org.ow2.asm:asm-util:9.2'
}

接下来,我们需要创建一个ASM的ClassVisitor,用于在方法调用前后插入代码。

public class SlowFunctionClassVisitor extends ClassVisitor {
    private String className;
    public SlowFunctionClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM9, classVisitor);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        className = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        return new SlowFunctionMethodVisitor(Opcodes.ASM9, mv, access, name, descriptor, className);
    }
    private static class SlowFunctionMethodVisitor extends AdviceAdapter {
        private final String methodName;
        private final String className;
        protected SlowFunctionMethodVisitor(int api, MethodVisitor mv, int access, String name, String descriptor, String className) {
            super(api, mv, access, name, descriptor);
            this.methodName = name;
            this.className = className;
        }
        private static final String startTimeFieldName = "_start_time";
        @Override
        protected void onMethodEnter() {
            //在方法进入时插入代码
            visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            visitLdcInsn("enter " + methodName);
            visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            visitVarInsn(Opcodes.ALOAD, 0);
            visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            visitFieldInsn(Opcodes.PUTFIELD, className, startTimeFieldName, "J");
        }
        @Override
        protected void onMethodExit(int opcode) {
            //在方法退出时插入代码
            visitVarInsn(Opcodes.ALOAD, 0);
            visitFieldInsn(Opcodes.GETFIELD, className, startTimeFieldName, "J");
            visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            visitInsn(Opcodes.LSUB);
            visitVarInsn(Opcodes.LSTORE, 2);
            Label l1 = new Label();
            visitVarInsn(Opcodes.LLOAD, 2);
        visitLdcInsn(100L);  // 100ms
        visitInsn(Opcodes.LCMP);
        visitJumpInsn(Opcodes.IFLE, l1);
        // 超过阈值,记录慢函数信息
        visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visitLdcInsn("exit " + methodName + " cost " + Long.toString(2L) + " ms");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        // 加载阈值
        visitLdcInsn("slow_function_threshold");
        visitMethodInsn(Opcodes.INVOKESTATIC, "android/content/res/Resources", "getSystem", "()Landroid/content/res/Resources;", false);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/content/res/Resources", "getAssets", "()Landroid/content/res/AssetManager;", false);
        visitLdcInsn("config.json");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/content/res/AssetManager", "open", "(Ljava/lang/String;)Ljava/io/InputStream;", false);
        visitTypeInsn(Opcodes.NEW, "org/json/JSONObject");
        visitInsn(Opcodes.DUP);
        visitTypeInsn(Opcodes.NEW, "java/io/InputStreamReader");
        visitInsn(Opcodes.DUP);
        visitVarInsn(Opcodes.ALOAD, 4);
        visitMethodInsn(Opcodes.INVOKESPECIAL, "java/io/InputStreamReader", "<init>", "(Ljava/io/InputStream;)V", false);
        visitMethodInsn(Opcodes.INVOKESPECIAL, "org/json/JSONObject", "<init>", "(Ljava/io/Reader;)V", false);
        visitLdcInsn("slow_function_threshold");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/json/JSONObject", "optInt", "(Ljava/lang/String;)I", false);
        // 比较耗时和阈值
        visitVarInsn(Opcodes.LLOAD, 2);
        visitInsn(Opcodes.LCMP);
        visitVarInsn(Opcodes.ILOAD, 5);
        Label l2 = new Label();
        visitJumpInsn(Opcodes.IF_ICMPLE, l2);
        // 超过阈值,写入本地JSON文件
        visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visitLdcInsn("write to local json file: " + methodName + " cost " + Long.toString(2L) + " ms");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        visitTypeInsn(Opcodes.NEW, "org/json/JSONObject");
        visitInsn(Opcodes.DUP);
        visitVarInsn(Opcodes.ALOAD, 0);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
        visitLdcInsn(methodName);
        visitVarInsn(Opcodes.LLOAD, 2);
        visitMethodInsn(Opcodes.INVOKESPECIAL, "org/json/JSONObject", "<init>", "()V", false);
        visitVarInsn(Opcodes.ASTORE, 6);
        visitTypeInsn(Opcodes.NEW, "java/io/FileWriter");
        visitInsn(Opcodes.DUP);
        visitLdcInsn("slow_function.json");
        visitMethodInsn(Opcodes.INVOKESPECIAL, "java/io/FileWriter", "<init>", "(Ljava/lang/String;)V", false);
        visitVarInsn(Opcodes.ASTORE, 7);
        // 将慢函数信息写入JSON文件
        visitVarInsn(Opcodes.ALOAD, 7);
        visitVarInsn(Opcodes.ALOAD, 6);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/json/JSONObject", "toString", "()Ljava/lang/String;", false);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/FileWriter", "write", "(Ljava/lang/String;)V", false);
        visitVarInsn(Opcodes.ALOAD, 7);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/FileWriter", "flush", "()V", false);
        visitVarInsn(Opcodes.ALOAD, 7);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/FileWriter", "close", "()V", false);
        visitLabel(l2);
    }
    super.visitInsn(opcode);
}
}

以上是一段使用ASM技术实现记录Android慢函数的代码。它通过在方法的字节码中插入代码来判断方法耗时,如果超过阈值,就记录慢函数信息,并将其写入本地JSON文件中。在实现过程中,使用了ASM的相关API来生成字节码,并在方法执行过程中进行修改。

流畅性监测

Activity、Service、Receiver 组件生命周期的耗时和调用次数也是我们重点关注的性能问题。

例如Activity的onCreate不应该超过 1 秒,不然会影响用户看到页面的时间。

Service 和 Receiver 虽然是后台组件,不过它们的生命周期也是占用主线程的,也是我们需要关注的问题。 对于组件生命周期我们应该采用更严格的监测,可以全量上报各个组件各个生命周期的启动时间和启动次数。 一般的做法是,通过编译时插桩来做到组件的生命周期监测。

FPS监测

我们需要采集的卡顿数据有: 卡顿次数/交互次数比,帧绘制时间采样. 处理方式可以参考下面的内容:

  1. 定义一个FPS监测类,该类中包含FPS计算的逻辑:
public class FPSMonitor {
    private static final long ONE_SECOND = 1000000000L;
    private long lastTime = System.nanoTime();
    private int frameCount = 0;
    private int fps = 0;
    public void update() {
        long currentTime = System.nanoTime();
        frameCount++;
        if (currentTime - lastTime >= ONE_SECOND) {
            fps = frameCount;
            frameCount = 0;
            lastTime = currentTime;
        }
    }
    public int getFps() {
        return fps;
    }
}

使用ASM字节码框架在编译期间对代码进行插桩,将FPS监测的逻辑插入到游戏或应用程序的主循环中:

public class GameLoop {
    private FPSMonitor fpsMonitor = new FPSMonitor();
    public void loop() {
        while (true) {
            long startTime = System.nanoTime();
            // 游戏或应用程序的主逻辑// ...// 在主循环中插入FPS监测逻辑
            fpsMonitor.update();
            int fps = fpsMonitor.getFps();
            System.out.println("FPS: " + fps);
            // 控制FPS为60long elapsedTime = System.nanoTime() - startTime;
            long sleepTime = (1000000000L / 60) - elapsedTime;
            if (sleepTime > 0) {
                try {
                    Thread.sleep(sleepTime / 1000000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

FPS监测逻辑被插入到了应用程序的主循环中,在每一帧结束时计算FPS,并将计算结果输出到控制台。我们使用插件可以将上述代码转换成字节码文件。转换代码如下:

public class FpsMonitorClassVisitor extends ClassVisitor {
    public FpsMonitorClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if (name.equals("loop")) {
            mv = new FpsMonitorMethodVisitor(mv);
        }
        return mv;
    }
    private static class FpsMonitorMethodVisitor extends MethodVisitor {
        public FpsMonitorMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM5, mv);
        }
        @Override
        public void visitCode() {
            super.visitCode();
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, "GameLoop", "fpsMonitor", "LFPSMonitor;");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "FPSMonitor", "update", "()V", false);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, "GameLoop", "fpsMonitor", "LFPSMonitor;");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "FPSMonitor", "getFps", "()I", false);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitInsn(Opcodes.SWAP);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
        }
    }
}

创建了一个 FpsMonitorClassVisitor 类来处理插桩逻辑,它继承自 ClassVisitor 类。

visitMethod 方法中,我们判断当前访问的方法是否为 loop 方法,如果是,则创建一个 FpsMonitorMethodVisitor 对象来处理该方法的插桩逻辑。

FpsMonitorMethodVisitor 中,我们将 FPS 监测逻辑插入到了游戏或应用程序的主循环中,在每一帧结束时计算 FPS,并将计算结果输出到控制台。

最后,我们将创建的 FpsMonitorClassVisitor 对象传递给 ASM 的 ClassReader,并通过 ClassWriter 来生成修改后的字节码。

Thread监测

由于文件IO开销、线程间的竞争或者锁可能会导致主线程空等,从而导致卡顿。我们可以借助BHook对线程进行监测,需要监测以下两点:

线程数量

需要监测线程数量的多少,以及创建线程的方式。例如有没有使用统一的线程池,这块可以通过 hook 线程的 nativeCreate() 函数,主要用于进行线程收敛,减少线程数量。

线程时间

监测线程的用户时间 utime、系统时间 stime 和优先级。主要是看哪些线程 utime+stime 时间比较多,占用了过多的 CPU。

上报时机

超过设置慢函数的阈值,开始收集慢方法函数和时间,开始上报。

业务降级

可通过参数配置平台配置开关,随时回归线上版本

注意事项

插桩方案问题 解决方案
包大小增加多少 过滤简单函数:i++,getter/setter支持黑名单配置包大小控制2%
只能监测到应用堆栈耗时,无法收集系统堆栈耗时 结合Looper方案,同时上报后台根据卡顿key来聚合分析
APP性能影响范围 非业务模块不要插桩,避免对稳定性造成影响

方案优化

为了提高自身性能,我们是不是可以同步线程方式进一步优化获取堆栈性能

getStackTrace

getStackTrace获取堆栈信息有如下两大业务痛点:

image.png

  1. 性能损耗
  2. 需要暂停主线程运行

为了解决上述业务痛点,我们可以采取ThreadDump和AsyncGetCallTrace两种解决方案。

方案 实现 特点
StackSampler 依赖SafePoint收集 公开API,稳定,有停顿
AsyncGetCallTrace SIGPROF定时器,Native层收集堆栈 非公开API,兼容性问题无停顿

StackSampler

在SafePoint处,JVM可以确保所有线程都停止执行,从而保证当前状态的一致性。利用SafePoint,我们可以实现无需挂起主线程的异步堆栈采样,从而避免了主线程被挂起的影响。

public class StackSampler {
    private static final int MAX_STACK_DEPTH = 32; // 最大堆栈深度
    private static final int MAX_STACK_SAMPLES = 100; // 最大缓存采样数
    private static final long SAMPLE_INTERVAL = 100L; // 采样间隔,单位:毫秒
    private final ConcurrentLinkedQueue<String> stackSamples; // 堆栈采样缓存
    private final AtomicBoolean profiling; // 采样标志位
    public StackSampler() {
        stackSamples = new ConcurrentLinkedQueue<>();
        profiling = new AtomicBoolean(false);
    }
    // SafePoint 采样方法
    private void sample() {
        if (profiling.compareAndSet(false,true)) {
            // 在 SafePoint 处异步采样堆栈信息
            new Thread(() -> {
                // 延迟一段时间,等待所有线程进入 SafePoint
                try {
                    Thread.sleep(SAMPLE_INTERVAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 采样堆栈信息并添加到缓存中
                String stackTrace = getStackTrace();
                stackSamples.offer(stackTrace);
                // 重置采样标志位
                profiling.set(false);
            }).start();
        }
    }
    // 获取当前线程的堆栈信息
    private String getStackTrace() {
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder();
        for (int i = 2; i < Math.min(stackTrace.length,,AX_STACK_DEPTH + 2); i++) {
            sb.append(stackTrace[i].toString()).append('\n');
        }
        return sb.toString();
    }
    // 输出所有采样结果
    public void dump() {
        int count = 0;
        String stackTrace;
        while ((stackTrace = stackSamples.poll()) != null && count < MAX_STACK_SAMPLES) {
            System.out.println(stackTrace);
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        StackSampler sampler = new StackSampler();
        sampler.sample(); // 开始采样
        Thread.sleep(MAX_STACK_SAMPLES * SAMPLE_INTERVAL); // 等待采样完成
        sampler.dump(); // 输出采样结果
    }
}

用SafePoint实现了异步堆栈采样,并在每次采样时延迟一段时间,等待所有线程进入SafePoint,以确保采样的堆栈信息是当前线程的真实状态。同时,采样的结果存储在一个线程安全的队列中,等待输出。当采样完成后,通过 dump() 方法输出所有的采样。

另外,为了避免频繁地采样堆栈信息导致性能问题,我们可以通过减少采样频率的方式进行优化。例如,可以通过将采样间隔从10毫秒调整为100毫秒,来减少采样的次数,从而降低对系统性能的影响。

另外,为了更高效地采样和处理堆栈信息,我们可以考虑采用缓存和批量处理的策略。例如,可以将采样的结果存储在一个缓存队列中,当队列达到一定大小时再进行批量处理,从而减少对队列的频繁访问和操作,提高程序的执行效率。

AsyncGetCallTrace

通过使用SIGPROF定时器,Native层收集堆栈方式,优化getStackTrace获取堆栈需要暂停主线程运行和相关性能问题

#include <signal.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <execinfo.h>
#define SAMPLE_INTERVAL 100 // 采样间隔,单位:毫秒
#define MAX_STACK_DEPTH 32 // 最大堆栈深度
#define MAX_STACK_SAMPLES 100 // 最大缓存采样数
volatile sig_atomic_t profiling = 0; // 采样标志位
// SIGPROF 信号处理函数
void profiling_handler(int signum) {
    profiling = 1; // 标记需要采样堆栈信息
}
// 开始采样
void start_profiling() {
    struct sigaction sa;
    struct itimerval timer;
    // 注册 SIGPROF 信号处理函数
    sa.sa_handler = profiling_handler;
    sa.sa_flags = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGPROF, &sa, NULL);
    // 设置定时器
    timer.it_value.tv_sec = SAMPLE_INTERVAL / 1000;
    timer.it_value.tv_usec = (SAMPLE_INTERVAL % 1000) * 1000;
    timer.it_interval = timer.it_value;
    setitimer(ITIMER_PROF, &timer, NULL);
}
// 停止采样
void stop_profiling() {
    struct itimerval timer;
    // 关闭定时器
    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = 0;
    timer.it_interval = timer.it_value;
    setitimer(ITIMER_PROF, &timer, NULL);
}
// 收集堆栈信息并输出到标准输出流
void dump_stack() {
    void *stack[MAX_STACK_DEPTH];
    int depth = backtrace(stack, MAX_STACK_DEPTH);
    if (depth > 0) {
        backtrace_symbols_fd(stack, depth, STDOUT_FILENO);
    }
}
int main() {
    start_profiling(); // 开始采样
    int sample_count = 0;
    while (sample_count < MAX_STACK_SAMPLES) {
        if (profiling) { // 如果需要采样堆栈信息
            profiling = 0; // 重置采样标志位
            dump_stack(); // 收集堆栈信息
            sample_count++; // 统计采样数
        }
    }
    stop_profiling(); // 停止采样
    return 0;
}

start_profiling()stop_profiling() 函数用于开启和关闭 SIGPROF 定时器,定时器的时间间隔由 SAMPLE_INTERVAL 宏定义指定。

profiling_handler() 函数是 SIGPROF 信号的处理函数,每次接收到信号后,将 profiling 变量置为 1。dump_stack() 函数用于收集堆栈信息,通过 backtrace() 函数获取堆栈信息,并输出到标准输出流中。

main() 函数中,循环收集堆栈信息,直到采样数达到最大值为止,最大采样数由 MAX_STACK_SAMPLES 宏定义指定。

通过 SIGPROF 定时器和信号处理函数的方式,可以在 Native 层收集堆栈信息,避免了在 Java 层获取堆栈信息的开销和性能问题。不过需要注意的是,堆栈采样对应用程序的性能有一定影响,需要权衡好采样间隔和采样深度等参数

九、总结展望

当测试提出卡顿问题,测试会新建Bug单给责任人处理。导致卡顿的原因有很多,比如函数非常耗时、I/O 非常慢、线程或锁间竞争等。

随着移动端用户越来越注重产品体验,APM系统也逐渐成为互联网公司重要基础设施。

卡顿是衡量App性能的一个重要指标,建设卡顿APM监测平台是Android卡顿优化长效治理关键。

同时,通过建设卡顿APM监测平台,帮助业务找到卡顿原因也是架构组TL考核评测员工重点OKR指向。

为了解决卡顿标准不明确问题,小木箱今天和大家着重探讨了卡顿监测的方方面面。

如果小木箱的文章对你有所帮助,那么欢迎关注小木箱的公众号:  小木箱成长营。我是小木箱,我们下一篇见~

参考链接



相关文章
|
17天前
|
XML 监控 安全
Android App性能优化之卡顿监控和卡顿优化
本文探讨了Android应用的卡顿优化,重点在于布局优化。建议包括将耗时操作移到后台、使用ViewPager2实现懒加载、减少布局嵌套并利用merge标签、使用ViewStub减少资源消耗,以及通过Layout Inspector和GPU过度绘制检测来优化。推荐使用AsyncLayoutInflater异步加载布局,但需注意线程安全和不支持特性。卡顿监控方面,提到了通过Looper、ChoreographerHelper、adb命令及第三方工具如systrace和BlockCanary。总结了Choreographer基于掉帧计算和BlockCanary基于Looper监控的原理。
24 3
|
2月前
|
存储 Android开发
Android 解决USB TP驱动中触摸卡顿和防抖动问题
Android 解决USB TP驱动中触摸卡顿和防抖动问题
62 1
|
2月前
|
Android开发
Android 新建一个lunch项(全志方案)
Android 新建一个lunch项(全志方案)
36 0
|
2月前
|
存储 XML 编译器
【Android 从入门到出门】第二章:使用声明式UI创建屏幕并探索组合原则
【Android 从入门到出门】第二章:使用声明式UI创建屏幕并探索组合原则
67 3
|
8月前
|
Android开发
[√]Android 通过adb内存监测方法
[√]Android 通过adb内存监测方法
327 1
|
10月前
|
存储 缓存 前端开发
Android Github 上面优秀的两种阴影方案,完美兼容高低版本问题
Android Github 上面优秀的两种阴影方案,完美兼容高低版本问题
|
11月前
|
Web App开发 编解码 网络协议
Android平台一对一音视频通话方案对比:WebRTC VS RTMP VS RTSP
Android平台一对一音视频通话方案对比:WebRTC VS RTMP VS RTSP
313 0
|
Java 测试技术 API
Android透明状态栏和导航栏方案最终版
Android透明状态栏和导航栏方案最终版
640 0
|
存储 Java Android开发
Android11.0(R) MTK 预置可卸载app恢复出厂不恢复(仿RK方案)
Android11.0(R) MTK 预置可卸载app恢复出厂不恢复(仿RK方案)
727 0