乐趣区

关于前端:中国大学-MOOC-Android-性能优化冷启动优化总结

分割咱们:
有道技术团队助手 :ydtech01 / 邮箱:ydtech@rd.netease.com

本文的重点在于如何定量的排查冷启动过程中的耗时操作,并提供对应的优化思路和实际办法总结。同时本文波及到的冷启动优化次要涵盖两个方面: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,并在控制台输入三个比拟要害的参数:

  • 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)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 视图,便可对程序运行的堆栈进行剖析:

上图就是 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 次冷启动耗时统计,后果如下:

启动速度 整体晋升 27%。

2.2.2 Application 优化

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

在 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 文件追踪:

能够看到,在 WelcomeActivity 的 onCreate 办法中,耗时较多的三个中央,别离是:initActionBar、EventBus.register、setContentView,上面针对这三块内容,别离进行对应的优化操作:

(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());
    }
}
退出移动版