浅谈Android启动优化

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_21983189/article/details/83151028

一、前言
随着我们的应用版本迭代,需要集成和增加的资源越来越多,尤其是在Application中,应用的性能也将出现很多需要优化的点。因此下面我们将从一个apk的启动原理去分析和解决启动时常常遇到的白屏、卡顿或者时间过长而带来的体验问题。

二、应用启动方式
1、冷启动
概念:是指启动应用时系统进程中没有该应用,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
特点:冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。
2、热启动
概念:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。
特点:当启动应用时,由于系统进程中已经有了该进程,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application,因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次。

以上两种情况,我们可以在application中通过Log日志去验证。

三、启动过程
解决问题之前我们有必要先了解一下Android应用的启动过程,而启动最慢、挑战最大的就是冷启动:系统和App本身都有更多的工作要从头开始!因此我们重点总结一下冷启动的过程:
点击桌面应用图标,系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后依次创建Application类、Activity类,加载主题样式Theme中的windowbackground等属性给activity,最后通过inflate加载布局,当oncreate/onstart/onresume等走完后最后进行contentView的measure/layout/draw显示在界面上,到此为止,应用的首次启动全部完成。

四、问题
我们通常在冷启动过程中碰到的白黑屏以及delay过长的问题存在于从Application初始化到界面完成contentView的绘制之间:
在这里插入图片描述

1、Application

UI上:给系统Theme设置windowbackground属性,用静态启动图的展示去掩盖数据的加载:

<application
        android:name=".function.application.AndroidApplication"
        android:theme="@style/AppThemeHxd">
</application>

.....

 <style name="AppThemeHxd" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="android:windowBackground">@mipmap/flash</item>
 </style>

逻辑上:UI伤的处理虽然看起来貌似启动很快,但是实际问题并未从根本上得到解决(治标不治本)。因此我们还的需要看实际代码,查找影响初始化的逻辑:
优化前:

class HxdApplication( ) {
    override fun onBaseContextAttached(base: Context?) {
        super.onBaseContextAttached(base)
        /**
         * You must install multiDex whatever tinker is installed!
         * 解决65535方法数超标的问题:采用 MultiDex 的 App 解压后可以看到有classes.dex,classes2.dex等多个dex文件,
         * 每个 dex 都可以最大承载 64k 个方法
         */
        MultiDex.install(base)
        //安装tinker
        Beta.installTinker(this)
    }

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            // crash catch init
            val crashHandler = CrashHandlerUtil.getInstance()
            crashHandler.init(applicationContext)

            // logger init
            val formatStrategy = PrettyFormatStrategy.newBuilder()
                    .showThreadInfo(false)  //(可选)是否显示线程信息。 默认值为true
                    //.methodCount(2)         // (可选)要显示的方法行数。 默认2
                    //.methodOffset(5)        // (可选)隐藏内部方法调用到偏移量。 默认5
                    //.logStrategy(customLog) //(可选)更改要打印的日志策略。 默认LogCat
                    .tag("HXD")   //(可选)每个日志的全局标记。 默认PRETTY_LOGGER
                    .build()
            Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))

            //init ARouter
            ARouter.openLog()
            ARouter.openDebug()
            ARouter.printStackTrace()
            //内存泄漏
            if (LeakCanary.isInAnalyzerProcess(instance)) {
                return
            }

            //严苛模式
            StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
                    .detectAll()//开启所有的detectXX系列方法
                    //.penaltyDialog()//弹出违规提示框
                    .penaltyLog()//在Logcat中打印违规日志
                    .build())
            StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
                    .detectActivityLeaks()//检测Activity泄露
                    .penaltyLog()//在Logcat中打印违规日志
                    .build())
        }

        initDeviceInfo()
        initInjector()
        //init ARouter
        ARouter.init(instance)
        // init user info
        initUserInfo()
        // 下拉刷新
        SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, layout ->
            StoreHouseHeader(context).initWithString("loony")
        }
        // init 第三方库
        ThirdInitUtil.thirdInitUtils(instance)
     }
    }

通过上面的代码我们可以看得出来Application的onBaseContextAttached()方法和onCreate()里干的事非常多:MultiDex分包、安装tinker、初始化CrashHandler、初始化logger、初始化ARouter、三方库StrictMode、初始化设备信息、初始化界面资源管理器Injector、初始化用户信息、初始化下拉刷新控件以及其他三方库的初始化!逻辑众多,任务繁重,严重拖累了Application的后腿!通过整理我们改造后的逻辑是这样的:
优化后:

Application

class HxdApplication( ) {
    override fun onBaseContextAttached(base: Context?) {
        super.onBaseContextAttached(base)
        MultiDex.install(base)
    }

    override fun onCreate() {
        super.onCreate()
        
		//TODO 为了不占用该app进程在启动时申请系统的空间和CPU运算资源,将以下三方资源的初始化放在线程里,然后将该线程的优先级降低,设置成后台运行
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
        launch {
            //安装tinker
            Beta.installTinker(this)
            //init ARouter 界面切换控制器
            ARouter.init(instance)
            // init 第三方库
            ThirdInitUtil.thirdInitUtils(instance)

            if (BuildConfig.DEBUG) {
                //init ARouter
                ARouter.openLog()
                ARouter.openDebug()
                ARouter.printStackTrace()

                // crash catch init
                val crashHandler = CrashHandlerUtil.getInstance()
                crashHandler.init()

                // logger init
                val formatStrategy = PrettyFormatStrategy.newBuilder()
                        .showThreadInfo(false)  //(可选)是否显示线程信息。 默认值为true
                        .tag("HXD")   //(可选)每个日志的全局标记。 默认PRETTY_LOGGER
                        .build()
                Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))

                //内存泄漏
                if (LeakCanary.isInAnalyzerProcess(instance)) {
                    return@launch
                }

                //严苛模式
                StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
                        .detectAll()//开启所有的detectXX系列方法
                        //.penaltyDialog()//弹出违规提示框
                        .penaltyLog()//在Logcat中打印违规日志
                        .build())
                StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
                        .detectActivityLeaks()//检测Activity泄露
                        .penaltyLog()//在Logcat中打印违规日志
                        .build())
            }
        }

        //TODO 将原来和activity相关的配置以及全局使用的变量初始化,延迟处理,因此放在了flash页里(WelcomeActivity)
    }

WelcomeActivity

class WelcomeActivity{
	 override fun initInjector() {
        //TODO 为了不阻碍该界面的打开速度,异步初始化全局的基础数据
        launch {
            // 下拉刷新
            SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, _ ->
                StoreHouseHeader(context).initWithString("loony")
            }
            initDeviceInfo()
            // init user info
            initUserInfo()
        }
        //界面资源初始化管理器
        initInjectorApplication()
    }
}

通过上面的代码我们很明确的可以看得出来原来全部在Application中干的事我们现在由Application和WelcomeActivity两者来处理,同时结合异步线程(launch表示协程)处理耗时的任务,达到了本质上的改善,总结一下有这几点:

(1)、减压。减少application中onBaseContextAttached的任务,结合这里就是去除tinker的安装操作,毕竟tinker的安装操作属于一个耗时的任务,并且在刚启动时不是那么紧急需要;
(2)、异步。将一些必须要在application的oncreate中初始化的逻辑放在线程里异步处理,同时为了不占用该app进程在启动时申请系统的空间和CPU运算资源,将以下三方资源的初始化放在线程里,然后将该线程的优先级降低,设置成后台运行,也就是在launch之前设置线程的属性为后台运行;
(3)、任务分解和延迟。将原来和activity相关的配置以及全局使用的变量初始化,延迟处理,这里是放在里欢迎界面里。注意这里的欢迎界面不管冷启动还是热启动还是免登录进入都会从这里路过,所以放在这里不会导致资源的遗漏。大家在具体优化自己的项目时根据自己项目的情况而决定这些配置资源放的位置。

2、Activity

说完了application的优化,我们下面简单在总结一下Activity的启动优化。这里我们可以从这几点来分析:

(1)、高效复用activity的四大启动模式:

android:launchMode=“standard” standard模式作为activity启动的默认模式,也是标准模式,它的原理是每次打开一个activity都会在应用进程中创建一个新的对象,不会做任何判断和检查,在栈中就是依次增加:

在这里插入图片描述

android:launchMode=“singleTop” singleTop模式表示每次启动Activity时ActivityManagerServer都会检测栈顶是否是该Activity,如果是,则不会二次创建;如果不是,则创建一个新的并放在栈顶:
在这里插入图片描述
最终顺序是Activity B、Activity A、Activity B和Activity C;

android:launchMode=“singleInstance” singleInstance模式表示将当前的Activity设置为单例模式存在于独立的一个任务栈中,不受其他界面和任务栈的影响,此模式要慎用!

在这里插入图片描述

android:launchMode=“singleTask” singleTask模式表示每次创建Activity时先判断该Activity在任务栈用是否存在,如果存在则将该Activity上面的其他Activity全部销毁并将该Activity置顶;如果不存在则创建一个新的:

在这里插入图片描述
最终只剩下Activity B和Activity C。

通过上面对Activity启动模式的理解,我们在每次创建Activity时要根据该Activity的作用设置它的启动模式,以达到Activity的快速启动和复用

(2)、减少布局文件中layout的层级
通常来说,我们一个界面的布局如果超过四层就会有卡帧或者界面打开延迟的问题,为了能够快速完成xml的加载和测绘,减少xml的布局层级既能提高界面测绘的速度,也能使得布局代码更加简洁明了(例如项目中的activity_idcardauth_default.xml、activity_commerce.xml等)。如果界面的模块较多,简易利用分解布局,例如include。

(3)、高效复用自定义样式
在一些界面的layout中,多个Button或者EditText的样式相似度很高,那么我们可以将公共的属性写在style中,某个控件有特殊的设置,可以在它标签里单独设置即可。

(4)、异步加载数据
在Activity的oncreate中原则上我们制作一些View的初始化操作,数据的加载一定要通过子线程去处理,不管是本地数据还是服务器数据,当然了一些耗时的I/O操作更要放在子线程里处理。对于这类有数据加载或者耗时操作的Activityoncreate中首先要考虑创一个loading悬浮框来缓解界面这时候的黑白屏的尴尬

(5)、onCreate()与onStart()配合使用
在一些任务多的Activity里,我们不必所有的任务都放在oncreate中去处理,onStart里也可以去分担一些任务,只不过在这里处理数据时要考虑到界面二次回退刷新时的逻辑,防止界面暂停时从别的界面回退回来重复处理。

(6)、减少Activity的父类继承层级
我们在创建一个Activity时通常会继承BaseActivity,而BaseActivity继承自android.support.v7.app.AppCompatActivity或者android.support.v4.app.FragmentActivity,而这俩继承自android.support.v4.app.SupportActivity或android.app.Activity,而它们继承自ContextThemeWrapper以及ContextWrapper,最终来自Context,这期间开发者要是在定义一些额外的Activity的拓展类,会严重影响最外层Activity的重载速度和打开时间。因此,我们能直接继承android.app.Activity的话,不建议再去继承其他的类。

(7)、给Activity的Theme设置转场动画
两个Activity之间切换,应用默认会使用项目build tool对应的版本里动画效果,在一些设备中由于api的原因,可能会强制变成系统的动画效果。因此,为了我们界面在切换时能够统一转场模式以及对转场时间的把控,增加一个系统转场动画能够有效解决问题,同时也能为下一个界面的打开赢得数据加载的缓冲时间。

(8)、在ImageView中深入理解background、src俩属性,布局中和代码中尽量保持属性使用的一致性,避免一个View中重复接收资源图,造成占用内存空间的浪费和界面显示时的一些异常:

src的代码实现:
在这里插入图片描述
background的代码实现:
在这里插入图片描述
以上这俩都是给imageView绘画,区别是src给内容绘画,而background是设置背景,资源占用的内存大小和效率都是一样的,因此我们要避免俩属性同时使用。

(9)、分页处理和分包处理
分页处理是说对于我们常见的列表型数据,在遇到条目内容比较丰富、加载资源多的情况下,我们不妨分页加载,延迟展示多余的内容,会大大提升用户体验和界面启动的速度;
分包处理主要指的是遇到接收到服务端的数据流非常大的时候,我们可以采用断点续传、及时回收I/O、单线程处理等措施达到内存占用的节省和CPU利用率的提升;在遇到查询数据库时,尽量然sql精简和轻量化,不可使用过多关系网,压缩数据源或者分批使用数据包;最后我们在上报数据和采集数据时避免将过多的I/O放在内存里,影响界面的启动和操作流畅度。

猜你喜欢

转载自blog.csdn.net/qq_21983189/article/details/83151028