4

中国大学 MOOC Android 性能优化:冷启动优化总结

 3 years ago
source link: http://www.androidchina.net/11880.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文的重点在于如何定量的排查冷启动过程中的耗时操作,并提供对应的优化思路和实践方法总结。同时本文涉及到的冷启动优化主要涵盖两个方面:Application 的性能优化和 Launcher Activity 的性能优化。

一、背景

中国大学 MOOC 是网易与高教社携手推出的在线教育平台,目前,经过长期的产品打磨和钻研,在课程数量、质量以及影响力,中国大学 MOOC 已成为全球领先的中文慕课平台。同时经过此次优化,冷启动速度整体提升27%。

在我们日常开发中,随着 app 整体迭代次数增多,由于长久以来的迭代需求,android app 本身也集成了较多的第三方组件和 SDK,同时在日常迭代中,也是以业务迭代需求实现为主要目的,导致现在 app 本身,或多或少存在一些性能可优化空间。所以有必要进行性能优化,提升用户体验。

此次优化,主要侧重于两个方面:

  • Application 的性能优化
  • app 启动页性能优化

该文档重点不在于代码规范和业务代码逻辑导致的性能问题,而是在假设代码无明显、严重性能漏洞,并且不改变原有业务逻辑,量化性能监测数据和问题,并针对其进行优化修改。

二、冷启动速度优化

2.1 相关知识点

2.1.1 冷启动耗时统计

adb shell am start -S -W [packageName]/[activiytName]

上述 adb 命令中,几个关键参数说明:

  • -S:表示启动该 app 前先彻底关闭当前 app 进程
  • -W:启动并输出相关耗时数据
  • packageName:app 的 applicationID
  • activityName:app 启动需要拉起的 Activity,如果用于统计冷启动耗时,那么该参数即为应用的第一个启动的 Activity(intent-filter 为 LAUNCHER 的 Activity)

再执行上诉 adb 后,会成功唤起 APP,并在控制台输出三个比较关键的参数:

图1.png

  • LaunchState:启动模式,上诉启动模式为冷启动
  • WaitTime:系统启动应用耗时= TotalTime +系统资源启动时间(单位 ms )
  • TotalTime:应用自身启动耗时=该 Activity 启动时间+应用 application 等资源启动时间(单位 ms )

对于应用层面得冷启动性能优化,我们关注的时间 TotalTime,该时间大致可以概括为:Application 构造方法→该 Activity 的 onWindowFocusChange 方法时间总和。而这个过程也可以粗略认知为,用户点击桌面图标到 app 第一个 Activity 获取焦点,业务代码执行的总时间(针对业务代码的优化,我们暂时不关心 Zygote 进程、Launcher 进程、AMS 进程的交互)。

2.1.2 冷启动耗时堆栈观察方法

在 Android API>=26 的系统版本中,建议使用 CPU Profile 或者 Debug.startMethodTracing 进行监控并导出 trace 文件进行分析。不管哪种方式,采集堆栈信息都有两种模式:采样模式和追踪模式。追踪模式会一直抓取数据,对设备性能要求较高。

(1)CPU Profile

图2.png

(2)Debug.startMethodTracing

由于冷启动涉及到业务应用层面的时间是:该 Activity 启动时间+应用 application 等资源启动时间,所以我们在 Application 构造方法中开始采集,在第一个 Activity 的 onWindowFocusChange 中停止采集,并输出 trace 文件。

/**
 * 在Application构造方法中开始采集
 */
public UcmoocApplication() {
    //保存Trace文件的目录
    File file = new File(Environment.getExternalStorageDirectory(), "ucmooc.trace");

    //采集方式有以下两种,根据需求选择其一
    //第一种:通过采样的方式,追踪堆栈信息
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        //通过采样方式追踪堆栈信息,需要指定文件保存目录、文件最大大小(单位M)、采样间隔(单位us)
        Debug.startMethodTracingSampling(file.getAbsolutePath(), 8, 1000);
    }

    //第二种:通过追踪的方式,全量采集堆栈信息
    Debug.startMethodTracing(file.getAbsolutePath());

    coreApplication = new CoreApplication();
}

/**
 * 在启动后的第一个Activity的onWindowFocusChanged中停止监听
 *
 * @param hasFocus
 */
@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    Debug.stopMethodTracing();
}

同时,由于该操作涉及到文件读写权限,需要手动授予 APP 该权限

2.1.3 .trace 日志文件阅读

在导出获取到 .trace 文件后,把 .trace 拖动至 androidStudio 编辑区;或者直接浏览 CPU Profile 视图,便可对程序运行的堆栈进行分析:

图3.png

上图就是 trace 文件打开后的效果,展示的是基于 CPU 使用和线程运行状况,针对启动速度的优化,需要关注的上图标注的几个点:

(1)CPU 运行时间轴:横向拖动可以选择查看的时间范围

(4)当前设备 CPU 轮转的线程:点击可以选择需要查看的线程,我们重点关注主线程

(2)当前选择线程,跟随时间轴,各个方法栈的调用情况和其耗时状况。其不同颜色分别代表

  • 黄色:android 系统方法(FrameWork 层代码,如果需要最终更底层的方法,需要最终 C/C++ 方法调用栈)
  • 蓝色:Java JDK 方法
  • 绿色:属于当前 app 进程执行的方法,包括一些类加载器和我们的业务代码(启动速度优化主要针对这一部分)

(3)各个方法栈的调用顺序和耗时情况,可以选择不用的排序方式和视图。

所以一般排查耗时方法时,建议先通过(2)视图直观检测到耗时较为严重的方法,锁定后,在(3)视图中查看具体的方法调用顺序。

2.2 优化步骤

由于在冷启动过程中,业务代码耗时主要集中在 Application 和 launcher Activity 中,所以优化过程也是分别针对这两块进行优化。

2.2.1 优化成果

使用2.1.1的方式,在优化前后,分别做了10次冷启动耗时统计,结果如下:

表格.png

启动速度整体提升 27%。

2.2.2 Application 优化

图4.png

通过 trace 文件,可以直观的发现,在 application 中,耗时最长的方法是其生命周期中的 onCreate 方法,其中在 onCreate 方法中,耗时比较长的方法有:initMudleFactory、initURS、Unicorn.init、initUmeng。

图5.png

在 Top Down 视图中,可以更加直观的看出,此次采样,也正是这四个方法耗时最多。

通过源码排查,这是个方法,均是第三方 SDK 的初始化,同时在这几个 SDK 内部,都含有较多的 IO 操作,并且内部实现了线程管理以保证线程安全,所以可以将这几个 SDK 的初始化,放在子线程中完成。这里以友盟 SDK 为例:

/**
* 友盟SDK中有涉及到线程不安全的地方,都自己维护了线程,保证线程安全
**/
try {
    var6 = getClass("com.umeng.umzid.ZIDManager");
    if (var6 == null) {
        Log.e("UMConfigure", "--->>> SDK 初始化失败,请检查是否集成umeng-asms-1.2.x.aar库。<<<--- ");
        (new Thread() {
            public void run() {
                try {
                    Looper.prepare();
                    Toast.makeText(var5, "SDK 初始化失败,请检查是否集成umeng-asms-1.2.X.aar库。", 1).show();
                    Looper.loop();
                } catch (Throwable var2) {
                }

            }
        }).start();
        return;
    }
} catch (Throwable var27) {
}

/**
* 在友盟SDK内部中有很多IO操作的地方,和加锁操作,所以可以将SDK初始化操作,放在子线程中
**/
if (!TextUtils.isEmpty(var1)) {
    sAppkey = var1;
    sChannel = var2;
    UMGlobalContext.getInstance(var3);
    k.a(var3);
    if (!needSendZcfgEnv(var3)) {
        FieldManager var4 = FieldManager.a();
        var4.a(var3);
    }

    synchronized(PreInitLock) {
        preInitComplete = true;
    }
}

最终,我们可以把上面提到的几个 SDK 初始化工作放入在子线程中:

private void initSDKAsyn(){
    new Thread(() -> {
        if (Util.inMainProcess()){
            // 登录
            initURS();

            if (BuildConfig.ENTERPRISE) {
                Unicorn.init(BaseApplication.this, "", QiyuOptionConfig.options(), new QiyuImageLoader());
                initModuleRegister();
            } else {
                Unicorn.init(BaseApplication.this, "", QiyuOptionConfig.options(), new QiyuImageLoader());
            }

            // 初始化下载服务
            try {
                initDownload();
            } catch (Exception e) {
                NTLog.f(TAG, e.toString());
            }
        }
        initModuleFactory();
        initUmeng();
    }).start();
}

对于一些必须在主线程中初始化完成的 SDK,可以考虑使用 IdleHandler,在主线程空闲时,完成初始化(关于 IdleHandler 会在下面讲到)。

2.2.3 Launcher Activity 优化

auncher Activity 是 WelcomeActivity,在对 Application 优化结束后,再对 WelcomeActivity 进行优化,还是和上路的思路一样,先通过 trace 文件追踪:

图6.png

可以看到,在 WelcomeActivity 的 onCreate 方法中,耗时较多的三个地方,分别是:initActionBar、EventBus.register、setContentView,下面针对这三块内容,分别进行对应的优化操作:

图7.png

(1)initActionBar

在上图中,可以看到,initActionBar 中最耗时的操作是 getSupportActionBar,通过研究代码发现,在WelcomeActivity中,并不需要操作 actionBar,所以直接复写父类方法,去掉 super 调用即可。

(2)EventBus.register

EventBus 注册时,性能较差,是因为在改过程中涉及到大量的反射操作,所以对性能损耗较大。通过查看官方文档,该问题在 EventBus3.0 中得到了很好的处理,主要是通过 apt 技术增加索引,提升效率。(当前项目未升级版本,待后期优化)

(3)setContentView

setContentView 是 Activity 渲染布局时的必要方法,其耗时的点在于,解析 xml 布局文件时,使用了反射,所以如果 xml 布局文件非常复查的时候,可以使用androidx.asynclayoutinflater:asynclayoutinflater进行异步加载 xml 文件,使用方式如下:

new AsyncLayoutInflater(this).inflate(R.layout.activity_welcome, null,
        (view, resid, parent) -> {
            setContentView(view);
        });

三、优化方法总结

上面针对冷启动优化是基于当前项目本身做的步骤,这里汇总一些冷启动通用的优化思路

(1)合理的使用异步初始化、延迟初始化和懒加载机制:主要针对 Application 中各种 SDK 的初始化

(2)在主线程中应当避免很耗时的操作,比如 IO 操作、数据库读写操作

(3)简化 launcher Activity 的布局结构,如果非常复杂的布局,可以有以下两种方式进行优化:

  • 建议使用约束布局(ConstraintLayout)来减少布局嵌套避免过度渲染。
  • 使用 androidx.asynclayoutinflater:asynclayoutinflater 进行异步加载 xml 文件。

(4)合理使用 IdleHandler 进行延迟初始化,使用方式如下:

/**
 * 需要在当前线程中处理耗时任务,并且并不需要马上执行的话,可以使用IdleHandler
 * 这样该任务可以消息队列空闲时,被处理
 */
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        //此处添加处理任务

        //返回值为false:则执行完毕之后移除这条消息,
        //返回值为true:则则执行完毕之后依然保留,等到下次空闲时会再次执行,
        return false;
    }
});

(5)开始严苛模式(StrictMode)

该模式并不能帮我们自动优化性能,而是可以帮助我们检测出我们可能无意中或者一些第三方 SDK 中做的会阻塞 Main 线程的事情(比如磁盘操作、网络操作),并将它们提醒出来,以便在开发阶段进行修复。其检测策略有线程检测策略和虚拟机检测策略,我们可以设置需要检测的操作,当代码操作违规时,可以通过 Logcat 或者直接崩溃的形式提醒我们,具体使用方式如下

/**
 * 开启严苛模式,当代码有违规操作时,可以通过Logcat或崩溃的方式提醒我们
 */
private void startStrictMode() {
    if (BuildConfig.DEBUG) { //一定要在Debug模式下使用,避免在生产环境中发生不必要的崩溃和日志输出

        //线程检测策略
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()  //检测主线程磁盘读取操作
                .detectDiskWrites() //检测主线程磁盘写入操作
                .detectNetwork() //检测主线程网络请求操作
                .penaltyLog() //违规操作以log形式输出
                .build());

        //虚拟机检测策略
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects() //检测SqlLite泄漏
                .detectLeakedClosableObjects() //检测未关闭的closable对象泄漏
                .penaltyDeath() //发生违规操作时,直接崩溃
                .build());
    }
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK