Android性能优化之启动速度优化

  • Post author:
  • Post category:其他


前言:

文本主要会介绍三大块:

1.简略介绍APP启动的完整流程,对整个流程有所了解,才知道在哪里可以进行优化。

2.一些常用的APP启动优化的方案,主要分为三大块优化方向。

3.一些不常见的APP启动优化的方案,甚至包含一些FW层的代码改动,有的可能是对应用开发者无效的,但是对于车载开发是有用的。

一安卓APP启动完整流程分析(冷启动)

图1:

主要分为三个阶段:

1.1 桌面点击APP图标,通知到AMS去完成应用进程的创建的流程

1.安卓系统启动后,拉起的第一个应用是桌面应用。它由SystemServer负责创建,并持有AMS的binder引用。

2.点击桌面图标后,Launcher会通过binder通知AMS启动该APP。

3.AMS会根据传递过来的信息查询APP应用进程在后台是否存在。如果在后台,则属于热启动,如果不在后台,则属于冷启动。优化的重点一般都是冷启动。

4.如果APP进程不存在。AMS主要会做两件事:

一:AMS首先会读取对应APP的Manifest信息(此配置信息是存在于AMS中,手机启动或者应用安装时读取到内存中的),然后根据MainActivity的主题设置,读取其背景图并展示到屏幕上。

二:通过socket的方式通知Zygote去fork产生APP进程。

如图2所示:

1.2 应用进程创建后的流程

5.APP进程创建后,会通过执行ActivityThread中的main方法。此方法主要做了两件事,

第一会进行Looper的初始化;

第二会通过attach方法通知AMS,进行进程的绑定,此时也会把APP创建的binder传递给AMS。

6.AMS中完成注册绑定后,会通过binder通知APP进行application的绑定。APP端binder的接收者是ApplicationThread(下同)。ApplicationThread会被调用bindApplication方法,然后通过handler通知ActivityThread去调用handleBindApplication方法。

7.handleBindApplication方法负责应用初始化的所有流程。主要流程图如下图所示:

图3:

首先,在方法中,通过ContextImpl.createAppContext(this, data.info)去加载DEX文件以及资源。

具体加载DEX的方法在LoadedApk的createOrUpdateClassLoaderLocked方法中:

private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
        ...

        //生成Classloader
            mDefaultClassLoader = ApplicationLoaders.getDefault().getClassLoaderWithSharedLibraries(
                    zip, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                    libraryPermittedPath, mBaseClassLoader,
                    mApplicationInfo.classLoaderName, sharedLibraries, nativeSharedLibraries);
            mAppComponentFactory = createAppFactory(mApplicationInfo, mDefaultClassLoader);

        ...
        //把上面生成的mDefaultClassLoader赋值给mClassLoader
        if (mClassLoader == null) {
            mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader,
                    new ApplicationInfo(mApplicationInfo));
        }
    }

第二,去执行Application的的attachBaseContext方法。

第三,执行installProvider方法,加载App中的各个ContentProvider

第四,执行Application的onCreate()方法

1.3 Activity的首屏展示流程

8.AMS通知APP创建Application后,还会通知APP进程去启动activity。这时候ApplicationThread接收到之后,会通知ActivityThread完成Activity的启动流程。这个根据android版本不同有区别,android12之后传递的是ClientTransaction。该对象包含一系列事务,对应的会通知构建activity的各个流程。

9.ActivityThread中会分别执行handleLaunchActivity,handleStartActivity,handleResumeActivity等方法,对应的会执行Activity的onCreate,onStart,onResume方法。

10.在handleResumeActivity方法中,执行完resume方法后,判断如何未关联到window上,则会把DecorView加到到ViewManager上。

public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
            boolean isForward, String reason) {
        ...
        if (!performResumeActivity(r, finalStateRequest, reason)) {
            return;
        }
        ...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }

        ...
    }

11.ViewManager的最终实现类是WindowManagerGlobal,在addView(DecorView)时,会创建ViewRootImpl负责后续绘制的完整流程。

12.添加完成后。ViewRootImpl会生成渲染任务TraversalRunnable,在收到垂直信号量之后执行。任务执行完成后,则首屏就显示在屏幕上了。

二.如何排查启动卡顿问题

2.1 使用SystemTrace的方式进行分析

2.1.1 如何得到冷启动总耗时的准确数据

首先的确保杀死应用,adb shell pkill beanlab (包名匹配到beanlab就杀掉)

然后使用 adb shell am start -W [包名]/[包名.Activity] 。

启动APP,查询App的启动时间

查询结果中,对应的时间参数详细解析如下:

ThisTime:对应activity启动耗时;

TotalTime:应用自身启动耗时 = ThisTime + 应用application等资源启动时间

WaitTime:系统启动应用耗时 = TotalTime + 系统资源启动时间

2.1.2 如何观察冷启动耗时的各个时间段的准确数据

通过抓取systrace可以从图上观察到 bindApplication ->activityStart->activityResume->Choreographer#doFrame

也可以添加自己的方法抓取,注意开始和结束必须成对出现


Trace.beginSection(“你的方法”);


Trace.endSection();

2.1.3 如何得到冷启动结束的回调

首先是display time:从Android KitKat版本开始,Logcat中会输出从程序

启动到某个Activity显示到画面上所花费的时间。这个方法比较适合测量程序的启动

时间。

筛选AcctivityManager: Displayed

ActivityManager: Displayed com.beantechs.beanlab/.ui.HomeActivity: +842ms 这个时间 和 adb启动分析的 TotalTime 一致

2.2 使用matrix框架

三.如何进行启动优化

第一章时,我们了解了一个APP启动的完整流程。对这个流程分析一下,我们可以住要分成以下三块:

1.从用户点击图标,到通知系统去创建APP进程。

2.APP进程创建后,通知AMS并且进行绑定并走Application的所有流程。

3.启动Activity的流程。

所以如何进行启动优化,也主要按照这三块分类去讲解。

3.1 优化APP进程创建之前的卡顿问题

这一块由于主要运行在系统层面,所以我们可优化的点不多。虽然我们不能彻底解决,但是还是可以一定程度上优化用户的体验。


3.1.1 通过预制图进行体验感觉上的优化。

第一章的时候我们讲过,AMS会根据传递过来的信息,会在启动APP进程之前,加载一张APP的背景图。所以我们可以通过提前加载预制图,让用户感官上知道我们APP已经启动。目前市场上大的APP都有设置预制图,比如支付宝的预制图就是以下这张:

配置方法如下:

在Manifest中,给Main的Activity设置theme即可。


<activity
    android:name=".SplashActivity"
    android:theme="@style/LoadingAppTheme"
    tools:replace="android:label">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

<style name="LoadingAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="windowNoTitle">true</item>
    <item name="windowActionBar">false</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:listDivider">@drawable/divider_line</item>
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowBackground">@drawable/launcher_previewwindow</item>
</style>

android12及以上的版本,支持黑白屏动画形式的展示,具体的使用方式这里就不展开了,原理是一样的。


将现有的启动画面实现迁移到 Android 12 及更高版本

3.2 优化应用初始化耗时问题

应用初始化的优化,主要其实就是就是对bindApplication这个方法的优化,通过打点日志分析,我们发现其实主要有以下几个耗时点,也是对应的我们的优化点。


3.2.1 优化APK加载

第一章时我们知道,加载DEX是在ContextImpl.createAppContext()的时候,自然也是启动主流程上。加载DEX的流程是从APK包中解压,然后ODEX优化,最后加载到内存当中。自然的,如果DEX越少,那么解压的就越少,ODEX优化的越少,速度也就越快。

所以我们优化的主要方法是拆分APP,也是通过组件化,插件化的方式去进行加载。

系统需要在启动的时候读取哪些内容呢?主要有两个部分,dex文件和资源文件。所以我们可以把一个很大的APK,按照业务拆分成多个小的APK。其中主APK中的Dex文件和资源弄的很小,其余业务APK放到asset文件夹中。等到APP启动后,再去解压加载业务APK。因为主APK很小,所以启动速度自然就会快得多。而等到应用启动后,再去通过懒加载的方式,逐渐加载其它模块的业务APK,因为在后台加载,所以也不会影响用户正常的操作,这就是我们组件化启动优化的方案。

结合实际项目,大多数APP其实都不大,所以并不太需要通过这种模块化的方案进行优化,所以就不详细讲如何优化了。有兴趣的可以看一下

Shadow



RePlugin

等框架。


3.2.2 ContentProvider优化

通过第一章的图3我们可以知道,在Application的启动流程中,会依次执行attachBaseContext,ContentProvider的onCreate,Application的onCreate方法。

所以ContentProvider的onCreate方法中,一定不能有耗时操作,否则会拖慢运行速度。


举例:

public class XXXProvider{
    ...
    @Override
    public boolean onCreate() {
        //context init
        ...
        //db init
        dbUtil = DatabaseUtil.getInstance();
        dbUtil.open();
        return true;
    }

    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable                     String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return dbUtil.fetchAll();
    }

    ...
}
我们可以知道,加载数据库是耗时操作,则我们应该挪到其他步骤当中。


解决问题:

ContentProvider的onCreate()是启动流程当中的,所以我们可以把加载数据库的耗时操作放到query中,或者等到启动完成后延时加载。

public class XXXProvider{
    ...
    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable                     String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        if(dbUtil==null){
            dbUtil = DatabaseUtil.getInstance();
            dbUtil.open();
        }
        return dbUtil.fetchAll();
    }

    ...
}


3.2.3 Application中onCreate优化

进行onCreate优化,核心是要梳理onCreate中我们到底做了什么。通过梳理可以发现,onCreate方法中,我们经常要初始化各种框架,比如Bugly.init(),LeakCanary.init()等等,这些框架虽然每个耗时并不多,但是因为是串行执行,累加起来,总耗时反而不少。


举例:

这里以某款APP为例,我们先查看其systrace文件(该文件在5.2当中)

其中bindApplication方法中有一部分如下图所示:

图中显示有加载多个不同SDK类的操作,而且是串行的结构,所以我们可以推断,应用在bindApplication的时候做了太多的初始化操作并且并行执行,导致耗时较多,而且bindApplication对应的是我们项目中Application的onCreate方法。


解决问题:

所以我们可以进行以下几点的优化:

1.部分任务不要主线程执行的,可以挪到子线程执行。

2.有些任务在子线程执行,但是依赖主线程某个任务执行完才可以。这种我们可以等到对应主线任务执行完再把任务加入到子线程池中。

当然,如果相互依赖的逻辑复杂,上面的方式就不太合适了,我们可以使用一个已经封装好的任务拓扑依赖框架来解决这问题:

android-startup


使用简介:

首先build.gradle中添加依赖:

implementation 'io.github.idisfkj:android-startup:1.1.0'

创建待执行的启动任务类:

class SampleFirstStartup : AndroidStartup<String>() {

     //是否主线程执行
    override fun callCreateOnMainThread(): Boolean = true

    //是否依赖主线程任务
    override fun waitOnMainThread(): Boolean = false

    //执行的初始化操作
    override fun create(context: Context): String? {
        // todo something
        return this.javaClass.simpleName
    }

    //依赖哪些其他任务
    override fun dependenciesByName(): List<String>? {
        return null
    }

}

然后我们在启动的时候,把这些StartUp组装一下就可以了。

class SampleApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        StartupManager.Builder()
            .addStartup(SampleFirstStartup())
            .addStartup(SampleSecondStartup())
            .addStartup(SampleThirdStartup())
            .addStartup(SampleFourthStartup())
            .build(this)
            .start()
            .await()
    }
}

或者在manifest中注册也可以

<provider
    android:name="com.rousetime.android_startup.provider.StartupProvider"
    android:authorities="${applicationId}.android_startup"
    android:exported="false">

    <meta-data
        android:name="com.rousetime.sample.startup.SampleFourthStartup"
        android:value="android.startup" />

</provider>

3.2.4 其它初始化时的耗时操作

1.SharedPreferences涉及到IO操作,所以如果SP存储数据较大的话,阻塞时间会较长

 SharedPreferences sp = getSharedPreferences("1", 0);
 sp.getString("1", "1");

3.3 解决Activity加载问题


3.3.1:主线程不执行耗时操作

通过第一章的流程讲解,我们可以知道在第一帧绘制之前,主线程会执行onCreate,onStart,onResume三个方法,所以这三个方法中,一定不能有耗时的方法。

如果一定需要主线程执行的,可以使用IdelHandler的方式解决(IdelHandler会在主线程不忙时执行)。

比如我们在加载数据的时候,就可以通过IdelHandler的方式来代替:

实例如下:

原代码:

@Override
protected void onResume() {
    super.onResume();
    ...
    loadData();
    ...
}

private void loadData() {
    ...
    组装请求..
    发送请求...
    解析数据...
}

改成:

@Override
protected void onResume() {
    super.onResume();
    ...
    MessageQueue.IdleHandler idleHandler = () -> {
        loadData();
        return false;
    };
    getMainLooper().getQueue().addIdleHandler(idleHandler);
   
    ...
}

private void loadData() {
    ...
    组装请求...
    发送请求...
    解析数据...
}


3.3.2:预加载页面

我们通过第二章的工具可以发现,Activity的几个生命周期中,onCreate方法一般是最为耗时的,而onCreate方法中,最为耗时的一般是setContentView(int)方法。

通过深入阅读源码可以可以发现,这其中最为耗时的部分就是把复杂的xml转化为ViewGroup对象。

这种问题,我们可以通过下面的方案来解决:

1. 使用AsyncLayoutInflater。

2.我可以做一个这样的操作,在Application的onCreate方法的任务中,添加一个这样的任务,子线程中把xml转化为ViewGroup。这样执行到Activity的onCreate方法中时,我们可以直接使用ViewGroup对象,从而节省了XML解析的时间。(如果布局文件中含有fragment不能采用此方案)

3.转Compose。Compose的话没有解析XML的时间,也不受到过多布局层级的影响。


3.3.3:局部加载优先显示框架或者占位图

还是通过第二章的工具,我们发现在项目中,measure/layout/draw也是很耗时的(其中一半measure是最耗时的)。这是因为我们布局太复杂了,导致界面渲染的时候需要反复计算,从而耗费时间。

所以针对这种情况,我们可以主要有4个方法来解决:

1.非主框架的部分使用ViewStub加载,等到主框架加载并显示出来后,再去加载内容的部分。我们经过测算,在第四帧之后,框架是可以完全显示出来的,所以在第四帧之后进行ViewStub内容的加载,是最为合适的。

2.降低布局层次嵌套和复杂度。复杂布局使用约束布局,尽量少使用weight属性等等。

3.解决过度绘制问题

4.优先加载占位图,等到内容显示好之后,在把占位图隐藏掉。


3.3.4:预加载数据

页面加载好了之后,自然就是请求网络加载数据了。

通过网络加载数据,大多数都是通过OKHttp进行请求。如果想更快的展示出来并且不在乎有效性,那么可以开启使用缓存,并且设置缓存有效时间。

但是这样也有个问题,如果设置了有效期,那么有效期内该请求全部都使用缓存。此时就无法获取最新的数据了,哪怕非首次请求也不行,因为服务并不会为你单独开辟一个新的接口。

我们可以实现这样的一个小需求:“首次请求使用缓存,非首次请求不使用缓存,并且还能把收到的响应更新到首次请求的缓存中”,这样就能比较好的解决上面所说的问题了。

具体例子可以参考我的另外一篇文章中的6.2。

OKHttp原理讲解之责任链模式及扩展_失落夏天的博客-CSDN博客_android okhttp责任链

3.4 其它优化

除了针对我们自身代码的优化,还有什么别的优化空间吗?当然有

3.4.1 Baseline Profile

这是google2022年开发者大会新提出的方案。

其核心原理是安卓7.0以后Android支持JIT,AOT并存的混合编译模式。

两者各有优势,JIT即时编译,虽然运行速度慢,但不需要编译时间。而AOT需要编译,后续运行速度快。

一般情况下,首次启动的时候会使用JIT编译,因为AOT需要转换,会导致首次启动耗时。后续使用的时候,安卓系统会根据使用频率计算出那些高频使用的代码,转换为AOT的方式进行编译加载,保证后续这块代码的运行速度。

而Baseline Profile就是需要我们自己把这些高频使用代码,提前打包到APK中,这样首次启动的时候,安卓会通过AOT转换为ODEX代码,以后使用这些可执行文件,速度上就会更快。

3.4.2 Hardcoder

这是腾讯开源的一个框架,核心原理是在需要手机性能时,主动通知系统去提升CPU频率,从而提升手机性能。而不需要性能时,则通知系统降频,避免手机电量的浪费。


https://github.com/Tencent/Hardcoder

3.4.3 Embryo方案

这是一加手机的一个方案,简单解释下就是在后台预创建一个进程,提前加载好资源。这样等到这个APP真的启动的时候,就可以直接使用,而不是重新创建了。

这里其实我有一个更简单有效的方案,我们知道,APP冷启动的时候,是System_server通知Zygote去fork应用进程的,应用进程创建后,再回掉通知System_server进程。至少在这段时间内,是没有绑定任何应用层信息的,也就是下图红框中的部分。

所以,就像APP中的预加载一样,我们为什么不能在系统层预加载一个APP进程呢?等到真的有APP应用创建需求的时候,直接去使用这个APP进程,而不是走创建流程创建一个。因为走的是socket通信,以及fork进程需要时间,所以整个流程有可能可以节省多达100ms的流程。这个方案还在调研探索中,目前理论上是可行的。

3.4.4 Redex方案

这个是facebook提出的一个方案,其实核心本质和proguard有一些类似,混淆字节码,让加载的DEX文件变的更小,则加载变得就更快。redex还有一个突出的亮点就是除了混淆之外,还能做一定的字节码层面的优化,比如A调用B,B调用C的场景,直接改成A调用C,这样减少了一层方法栈,执行速度肯定是更快的。

当然,这个的目标就不单纯的只是解决启动速度了,而是让APP运行的更快乐。

但是这个方法已经已经推出了很久了,到目前为止并不是很流行,其原因也是这样的操作容易引起各种各样的问题。

四.声明和备注

4.1 声明

文章中的原理和资料来自于网上搜索的资料,以及针对安卓源码的调试所得。基于的安卓版本是12。如有描述不准确的地方,或者好的建议,欢迎指出来。



版权声明:本文为AA5279AA原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。