如何应对 Android 面试官 -> 卡顿和布局如何优化?玩转 BlockCanary,手写核心实现

前言


在这里插入图片描述

本章我们来讲一下卡顿和布局如何进行优化?

线上卡顿如何监控?


先来讲解一些基础知识,便于我们理解卡顿监控

CPU 性能

造成卡顿的原因可能有千百种,不过最终都会反映到 CPU 时间上。我们可以把 CPU 时间分为两种:用户时间和系统时间

  • 用户时间就是执行用户态应用程序代码所消耗的时间;
  • 系统时间就是执行内核态系统调用所消耗的时间,包括 I/O、锁、中断以及其他系统调用的时间;

卡顿问题分析指标

CPU 使用率

我们可以通过 /proc/stat 得到整个系统的 CPU 使用情况,通过 /proc/[pid]/stat 可以得到某个进程的 CPU 使用情况

  • proc/self/stat: utime: 用户时间,反应用户代码执行的耗时 stime: 系统时间,反应系统调用执行的耗时 majorFaults:需要硬盘拷贝的缺页次数 minorFaults:无需硬盘拷贝的缺页次数;
  • top 命令可以帮助我们查看哪个进程是 CPU 的消耗大户;
  • vmstat 命令可以实时动态监视操作系统的虚拟内存和 CPU 活动;
  • strace 命令可以跟踪某个进程中所有的系统调用;

CPU 饱和度

CPU 饱和度反映的是线程排队等待 CPU 的情况,也就是 CPU 的负载情况。

  • CPU 饱和度首先会跟应用的线程数有关,如果启动的线程过多,容易导致系统不断地切换执行的线程,把大量的时间浪费在上下文切换,我们知道每一次 CPU 上下文切换都需要刷新寄存器和计数器,至少需要几十纳秒的时间;
  • CPU 饱和度和线程优先级也有关;
    • 线程优先级会影响 Android 系统的调度策略,它主要由 nice 和 cgroup 类型共同决定。nice 值越低,抢占 CPU 时间片的能力越强。当 CPU 空闲时,线程的优先级对执行效率的影响并不会特别明显,但在 CPU 繁忙的时候,线程调度会对执行效率有非常大的影响;
  • 我们可以通过使用 vmstat 命令或者 /proc/[pid]/schedstat 文件来查看 CPU 上下文切换次数,这里特别需要注意 nr_involuntary_switches 被动切换的次数;
    • proc/self/sched: nr_voluntary_switches: 主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是IO;
    • nr_involuntary_switches: 被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPU;
    • se.statistics.iowait_count:IO 等待的次数;
    • se.statistics.iowait_sum: IO 等待的时间;

卡顿问题分析工具-systrace

卡顿分析,不仅仅只有 systrace

安装教程

要使用Systrace,需要先安装 Python2.7。安装完成后配置环境变量path ,随后在命令行输入: python – version 进行验证;

Systrace具体使用可以查看博客:使用

Androidr sdk 中的 platform-tools 下的 systrace.py

就是一个 python 脚本,直接

python systrace.py

执行 --help 后会提供一些帮助命令

在这里插入图片描述

执行 --list-categories 会提供一些查看命令

gfx 
  Graphics 图形系统,包括SerfaceFlinger,VSYNC消息,Texture,RenderThread等
input
	Input输入系统,按键或者触摸屏输入;分析滑动卡顿等
view
	View绘制系统的相关信息,比如onMeasure,onLayout等;分析View绘制性能
am
	ActivityManager调用的相关信息;分析Activity的启动、跳转
dalvik
	虚拟机相关信息;分析虚拟机行为,如 GC停顿
sched
	CPU调度的信息,能看到CPU在每个时间段在运行什么线程,线程调度情况,锁信息
disk
	IO信息
wm
	WindowManager的相关信息
res
	资源加载的相关信息

使用方式

python systrace.py -t 8 -o test_trace.html gfx input view am dalvik sched wm disk res -a com.llc.example

默认是写将文件创建到了 systrace 所在目录下,可以将它移动到想要的位置;

然后我们用 chrome://tracing 打开这个 html 文件

在这里插入图片描述

建议使用 Perfetto UI 来分析;打开之后,我们要重点关注的就是 Frame 下的相关信息

在这里插入图片描述

绿色表示 帧间隔在 16ms,黄色红色表示 帧间隔大于 16ms,偶尔的一次黄色 偶尔的一次红色其实是没有太大关系的,连续的红色或者黄色 表示卡顿;

这里我们需要了解下 Android 底层渲染机制;

在这里插入图片描述

这个 16.6 代表着一个 VSync 的同步信号,每隔这样的一个时间点就会有一个脉冲,每一帧都在这个16.6ms 超过这个时间就会有肉眼可见的卡顿,当有 同步信号 过来的时候,首先会进行 input、动画的处理,通过 CPU 在 UI 线程计算完成之后,它会把这个计算结果给到 RenderThread,这里就涉及到了跟 GPU 跟屏幕硬件相关的绘制逻辑;

所以,我们的 trace 文件中会有 UI Thread 和 RenderThread;

我们在工作中,当用 systrace 追踪启动优化的时候,同时也需要借助 Trace 类下的两个方法,用来进行 tag 的标记;

protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    Trace.beginSection("cool start");
}

在首个 Activity 的 onWindowFocusChanged 回调中执行结束点

override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    Trace.endSection()
}

这样,在我们生成的 trace 文件中就能看到这两个之间的耗时,这样,我们就能查看我们的启动耗时并进行优化;

在这里插入图片描述

如何用 systrace 看线上 release 包?

可以通过反射 setAppTracingAllowed 来实现线上 release

/**
 * From Android S, this is no-op.
 *
 * Before, set whether application tracing is allowed for this process.  This is intended to be
 * set once at application start-up time based on whether the application is debuggable.
 *
 * @hide
 */
@UnsupportedAppUsage
public static void setAppTracingAllowed(boolean allowed) {
    nativeSetAppTracingAllowed(allowed);
}

线上卡顿如何监控

线上监控方案有:消息队列、线程监控,插桩等等

消息队列 (Looper Printer 时间差)

利用UI线程Looper打印的日志匹配(通过替换 Looper 的 Printer 实现)

在这里插入图片描述

可以看到,消息分发之前和之后都有一个打印,我们可以接管系统中的这个 logging,当执行到 println 的时候,执行我们自己的 println 方法来记录这两个之间(msg.target.dispatchMessage())的间隔是否大于 16ms,大于则认为发生了卡顿;

Looper.getMainLooper().setMessageLogging(new Printer() {
    @Override
    public void println(String x) {
        
    }
});

通过这个方法来接管 Printer; BlockCanary 也是基于这个的实现原理;

但是这种方案的弊端就是:可能不准,再一个就是时间区间内,不能精确定位到具体是哪里发生了卡顿,需要再精细化分析;

Choreographer.FrameCallback

设置FrameCallback来接收编舞者的callback回调。记录两次vsync的时间差,大于了16ms ,超过阈值认为卡顿

在这里插入图片描述

监控线程

一个监控线程,每隔 1 秒向主线程消息队列的头部插入一条空消息。假设 1 秒后这个消息并没有被主线程消费掉,说明阻塞消息运行的时间在 0~1 秒之间。换句话说,如果我们需要监控 3 秒卡顿(也可以是 5s ANR),那在第 4 次轮询中头部消息依然没有被消费的话,就可以确定主线程出现了一次 3 秒以上的卡顿(也可以是 5s ANR);

在这里插入图片描述

这个方案也存在一定的误差,那就是发送空消息的间隔时间。但这个间隔时间也不能太小,因为监控线程和主线程处理空消息都会带来一些性能损耗,但基本影响不大;

这个通常用来做 ANR 的监控

插桩

使用 Inline Hook 技术,编译过程中插桩,在函数入口和出口加入耗时监控的代码。我们可以实现类似 Nanoscope 先写内存的方案;

  • 避免方法数暴增。在函数的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的 ID 作为参数;
  • 过滤简单的函数。过滤一些类似直接 return、i++ 这样的简单函数,并且支持黑名单配置。对一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗;

在这里插入图片描述

布局加载优化

说起布局加载优化,那么就需要了解布局的加载原理,只有懂了其中的原理,我们才能有针对性的进行加载优化,布局如何加载,可以看下我前面讲插件化换肤的文章;

我们从插件化换肤的方案中,利用 setFactory2 这个方法来获取到布局加载的耗时;

LayoutInflaterCompat.setFactory2(layoutInflater, object : Factory2{
    override fun onCreateView(
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? {
        val time = System.currentTimeMillis()
        val view: View = this@MainActivity.getDelete().createView(parent, name, context, attrs)
        LogEx.d(TAG, "name cost: " + (System.currentTimeMillis() - time))
        return view
    }

    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
        TODO("Not yet implemented")
    }
})

当我们拿到每一个 View 的创建耗时的时候,就可以 case by case 的去进行优化了;

优化方式

  • 这也是掌阅开源的 X2C 的原理,不采用反射的方式创建 View,通过 APT + javaPoet + new 技术生成对应的 java 文件并映射;
public static void setContentView(Activity activity, int layoutId) {
    if (activity == null) {
        throw new IllegalArgumentException("Activity must not be null");
    }
    View view = getView(activity, layoutId);
    if (view != null) {
        activity.setContentView(view);
    } else {
        activity.setContentView(layoutId);
    }
}    

getView 就是通过 new 的方式替换反射的方式创建 View,然后通过 setContentView 重新设置回去;

    public static View getView(Context context, int layoutId) {
        IViewCreator creator = sSparseArray.get(layoutId);
        if (creator == null) {
            try {
                int group = generateGroupId(layoutId);
                String layoutName = context.getResources().getResourceName(layoutId);
                layoutName = layoutName.substring(layoutName.lastIndexOf("/") + 1);
                String clzName = "com.zhangyue.we.x2c.X2C" + group + "_" + layoutName;
                creator = (IViewCreator) context.getClassLoader().loadClass(clzName).newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }

            //如果creator为空,放一个默认进去,防止每次都调用反射方法耗时
            if (creator == null) {
                creator = new DefaultCreator();
            }
            sSparseArray.put(layoutId, creator);
        }
        return creator.createView(context);
    }
  • AsyncLayoutInflater

在这里插入图片描述

原理就是:通过 InflaterThread 在子线程中执行 inflate 操作,然后通过 handler 将结果 post 到主线程,所以需要在 OnInflatFinished 回调中初始化 View;

但是 这种方案也有弊端,官方也并没有建议必须使用,例如 xml 中有 Fragment 是不支持的;

BlockCanary 原理


BlockCanary的原理:实现Printer接口,并通过Looper().getMainLooper().setMessageLogging() 来接管系统中的 logging,这样,当执行到 logging.println(“”) 的时候,就会执行我们自己的 println 方法

手写实现

public class BlockCanary {

    public static void install() {
        LogMonitor logMonitor = new LogMonitor();
        Looper.getMainLooper().setMessageLogging(logMonitor);
    }
}

LogMonitor 实现了 Printer 接口,并重写了 println 方法;

public class LogMonitor implements Printer {

    private StackSampler mStackSampler;
    private boolean mPrintingStarted = false;
    private long mStartTimestamp;
    // 卡顿阈值,超过3s 则认为发生了卡顿
    private long mBlockThresholdMillis = 3000;
    //采样频率
    private long mSampleInterval = 1000;

    private Handler mLogHandler;

    public LogMonitor() {
        mStackSampler = new StackSampler(mSampleInterval);
        HandlerThread handlerThread = new HandlerThread("block-canary-io");
        handlerThread.start();
        mLogHandler = new Handler(handlerThread.getLooper());
    }

    @Override
    public void println(String x) {
        // 从 if 到 else 会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息
        if (!mPrintingStarted) {
            //记录开始时间
            mStartTimestamp = System.currentTimeMillis();
            mPrintingStarted = true;
            mStackSampler.startDump();
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            // 出现卡顿
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            mStackSampler.stopDump();
        }
    }

    private void notifyBlockEvent(final long endTime) {
        mLogHandler.post(new Runnable() {
            @Override
            public void run() {
                //获得卡顿时主线程堆栈
                List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
                for (String stack : stacks) {
                    Log.e("block-canary", stack);
                }
            }
        });
    }


    private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
}

StackSampler 就是 dump 卡顿信息;

public class StackSampler {
    public static final String SEPARATOR = "\r\n";
    public static final SimpleDateFormat TIME_FORMATTER =
            new SimpleDateFormat("MM-dd HH:mm:ss.SSS");


    private Handler mHandler;
    private Map<Long, String> mStackMap = new LinkedHashMap<>();
    private int mMaxCount = 100;
    private long mSampleInterval;
    //是否需要采样
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

    public StackSampler(long sampleInterval) {
        this.mSampleInterval = sampleInterval;
        HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
        handlerThread.start();
        mHandler = new Handler(handlerThread.getLooper());
    }

    public void startDump() {
        //避免重复开始
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);
        mHandler.removeCallbacks(mRunnable);
        mHandler.postDelayed(mRunnable, mSampleInterval);
    }


    public void stopDump() {
        if (!mShouldSample.get()) {
            return;
        }
        mShouldSample.set(false);

        mHandler.removeCallbacks(mRunnable);
    }

    public List<String> getStacks(long startTime, long endTime) {
        ArrayList<String> result = new ArrayList<>();
        synchronized (mStackMap) {
            for (Long entryTime : mStackMap.keySet()) {
                if (startTime < entryTime && entryTime < endTime) {
                    result.add(TIME_FORMATTER.format(entryTime)
                            + SEPARATOR
                            + SEPARATOR
                            + mStackMap.get(entryTime));
                }
            }
        }
        return result;
    }

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString()).append("\n");
            }
            synchronized (mStackMap) {
                //最多保存100条堆栈信息
                if (mStackMap.size() == mMaxCount) {
                    mStackMap.remove(mStackMap.keySet().iterator().next());
                }
                mStackMap.put(System.currentTimeMillis(), sb.toString());
            }

            if (mShouldSample.get()) {
                mHandler.postDelayed(mRunnable, mSampleInterval);
            }
        }
    };
}

好了,卡顿优化就写到这里吧~

下一张预告


继续性能优化

欢迎三连


来都来了,点个关注,点个赞吧

参考资源链接:[BlockCanaryAndroid O及以上系统中的卡顿监控适配方案](https://wenku.youkuaiyun.com/doc/6cizvpq01p?utm_source=wenku_answer2doc_content) 在Android O系统中实现应用卡顿监控并适配BlockCanary,关键在于理解Android O的新特性以及如何将BlockCanary集成到应用中进行性能优化。首先,开发者需要熟悉Android O引入的特性,包括后台执行限制(Background Execution Limits)应用通知渠道(App Notification Channels),这些特性可能影响到应用的性能监控用户体验。 接着,我们需要将BlockCanary集成到Android应用项目中。通常情况下,可以在build.gradle文件中添加相应的依赖,并在Application的onCreate方法中初始化BlockCanary。由于Android O及以上系统对于后台服务应用权限有更加严格的要求,需要在AndroidManifest.xml中进行相应配置,确保BlockCanary能够正常工作。 为了适配Android O系统,需要特别注意以下几个方面: 1. 调整BlockCanary的工作模式,例如,可能需要使用前台服务来防止系统因为后台服务运行时间过长而杀掉监控服务。 2. 对于BlockCanary的UI组件,需要确保它们在Android O中能够正常显示,并且不违反窗口动画窗口过渡的系统限制。 3. 检查BlockCanary的权限申请,更新到符合Android O权限模型的最新版本,避免因权限问题导致监控失败。 最后,通过监控获取到的数据进行分析,定位到导致卡顿的具体代码位置,然后对代码进行优化。这可能涉及到减少主线程的工作量,避免耗时的网络请求磁盘操作,以及合理使用线程池等技术手段。 在完成上述适配优化工作后,可以通过实际测试来验证监控优化的效果,确保应用在Android O系统上运行流畅,用户体验得到提升。为了深入了解Android O系统适配BlockCanary的使用细节,推荐阅读《BlockCanaryAndroid O及以上系统中的卡顿监控适配方案》。这份资源将会提供更加详尽的操作指南技巧分享,帮助开发者在实际开发过程中有效地应用这些知识。 参考资源链接:[BlockCanaryAndroid O及以上系统中的卡顿监控适配方案](https://wenku.youkuaiyun.com/doc/6cizvpq01p?utm_source=wenku_answer2doc_content)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值