1. 运用发动的办法

在Android中,运用发动一般可分为三种:冷发动温发动热发动

那么什么是冷发动温发动热发动呢?下面咱们来简略看一下它们的定义:

  • 冷发动:当发动运用时,后台没有该运用的进程。这时体系会又一次创立一个新的进程分配给该运用,这个发动办法便是冷发动。

  • 温发动:当发动运用时,后台已有该运用的进程,可是Activity或许因为内存不足被收回。这样体系会从已有的进程中来发动这个Activity,这个发动办法叫温发动。

  • 热发动:当发动运用时,后台已有该运用的进程,且Activity依然存在内存中没有被收回。这样体系直接把这个Activity拉到前台即可,这个发动办法叫热发动。

因为冷发动相关于其他发动办法多了进程的创立(Zygote进程fork创立进程)以及运用的资源加载和初始化(Application的创立及初始化),所以相对来说会比较耗时,所以咱们一般说的App发动优化一般指的都是App的冷发动优化。

2. 优化方案

App发动优化的实质便是:发动速度和体会的优化

这就比如早些年你去饭馆吃饭,你想要点餐但等了半响都没有服务人员过来,或许就等得不耐心直接走开了。相同的,关于APP来说,假如用户点击App后长时刻都打不开,用户就很或许失掉耐心而卸载运用。

所以发动速度是用户对咱们App的榜首体会。假如发动速度过慢,用户榜首印象就会很差,这样即便你功用做出花来,用户也不会愿意去运用。

其实要想优化App的发动体会,要害便是要让用户更快地获取到运用的内容(流通,不卡顿、不等候),那么咱们应该怎么做呢?

2.1 事例剖析

这儿咱们还是先以之前说的去饭馆吃饭为例来展开讨论。现在的饭馆竞赛是尤为得剧烈,为了能够进步顾客的体会、留住顾客,真的是使出了浑身解数,除了口味之外,服务品质也是被摆在了越来越重要的方位。

就比方说:

  • 为了能够更快地给用户供给点餐服务,现在的饭馆每个座子上根本都贴上了点餐的二维码。
  • 一些热门需求排队的饭馆,在前台张贴了二维码,供给排队取号和提早点餐服务。
  • 一些常年爆满需求排队的饭馆,还供给了零食、茶水、棋牌以及美甲等服务。
  • 有些餐厅为了进步上菜的速度和体会,设置了倒计时免单的服务。

以上的改善办法,都是这些年餐饮行业在竞赛下,不断进步服务质量的产物。能够说谁的服务质量落下了,谁就有或许被淘汰。

其实咱们仔细剖析一下上面所罗列的,不难看出有很多是能够让咱们借鉴的。

(1)事例1

剖析:饭馆供给了点餐的二维码,实质上是将一件被迫等候转变为主动恳求的一种进程,客观上削减了等候的时刻,然后进步了速度。

类比:这对应咱们的运用程序,就像原先一些耗时不必要的三方库需求被迫等候其初始化结束程序才会继续进行,转变为先不初始化这部分耗时的三方库,等真正用届时再进行初始化;又相似咱们运用程序的游客模式,无需被迫进行一堆杂乱的用户注册进程,就能够直接进入程序运用,待触及一些用户信息功用的时分再提示用户注册。

(2)事例2

剖析:热门饭馆一起供给排队取号和提早点餐服务,实质上是将原先无法一起进行的操作(需求先排上队等到座位,才能扫描座位号点餐)变成了可一起进行的操作,将串行使命转化为并行使命,然后节约了时刻。

类比:这对应咱们的运用程序,便是将一些原本在主线程串行履行的耗时资源/数据加载,改为在子线程中并发履行。这在几个耗时使命耗时差距不大的时分优化尤为显着。

(3)事例3

剖析:在顾客排队等候的时分,供给零食、茶水、棋牌以及美甲等服务,实质上便是让顾客提早享用本饭馆的服务,然后缓解顾客等候的焦虑。

类比:这对应咱们的运用程序,便是开屏发动页。在Android12上,Google强制添加了这个开屏页,便是为了让用户提早看到你的运用页面,让用户产生运用发动很流通的假象,然后进步用户的发动体会。

(4)事例4

剖析:设置倒计时免单服务,实质上便是给等候加上了进度条上限以及超时补偿。俗话说最让人惧怕的是等候,比等候更让人惧怕的是看不见尽头的等候。而为等候设置上限,能够极大地缓解顾客等候的焦虑,究竟等候超时了也是会有补偿的。

类比:这对应咱们的运用程序,便是一些运用(比如游戏)初度发动会十分耗时,所以它们一般会在发动页添加一个初始化/加载进度条页面,来告知用户啥时分能加载完,而不是无止境未知的等候。

经过剖析,咱们能够看到(1)、(2)两种操作是从技能的视点来完成的优化,而(3)、(4)两种操作则更多的是从事务的视点去完成的优化。

2.2 优化战略

从上面的事例剖析,咱们能够得出,运用发动优化,咱们能够分别从技能和事务的视点来进行决策。

2.2.1 技能优化

(1)针对发动流程使命进行整理。

  • 无耗时使命 -> 主线程
  • 耗时且需求同步使命 -> 异步线程 + 同步锁
  • 耗时且无需同步使命 -> 异步线程

(2)非必要不履行。

  • 非必要数据 -> 懒加载
  • 非必要使命 -> 推迟/空闲履行
  • 非必要界面/布局 -> 推迟加载
  • 非必要功用 -> 删去/插件化

(3)数据结构优化,减小初始化时刻。

  • 数据结构尽或许复用,防止内存颤动
  • 数据结构申请的空间要恰到优点,不能过大(占用空间,创立慢)也不能过小(频繁扩大,功率低)
  • 关于必要且量大的数据,可采取分段加载。
  • 重要的资源本地缓存

2.2.2 事务优化

(1)事务流程整合。

  • 多个相关的串行事务整合为一致的一个事务。
  • 不相关的串行事务整合为并行的事务。

(2)事务流程拆分调整。

  • 对事务进行拆分,拆分出主要(必要)事务和次要(非必要)事务。
  • 分别对主要事务和次要事务进行优先级评估,事务履行按优先级从高究竟依次履行。

2.3 优化方向

在优化之前,让咱们先来剖析一下冷发动的进程:

Zygote创立运用进程 -> AMS恳求ApplicationThread -> Application创立 -> attachBaseContext > onCreate -> ActivityThread发动Activity -> Activity生命周期(创立、布局加载、屏幕安置、首帧绘制)

以上进程,只要Application和Activity的生命周期这两个阶段对咱们来说是可控的,所以这便是咱们的优化方向。

3. 优化办法

3.1 发动流程优化

1.依据之前咱们罗列的技能优化战略,首要需求对发动的一切使命流程进行整理,然后对其履行办法进行优化。

  • 只要必要且非耗时的使命在主线程履行。
  • 耗时且需求同步的使命,运用异步线程 + 同步锁的办法履行。
  • 耗时且无需同步的使命在异步线程履行。

2.第三方SDK初始化优化。

  • 关于那些在发动时非必要的第三方SDK,能够推迟初始化。
  • 关于初始化耗时的第三方SDK,能够开启一个后台服务/异步线程进行初始化。

3.运用使命履行结构。

这儿咱们还能够运用一些第三方的使命发动结构,对发动流程进行优化。下面我就拿我开源的XTask 简略介绍一下:

这儿咱们模仿了三种类型的使命:

  • 1.优先级最重要的使命,履行时刻50ms;
  • 2.单独的使命,没有履行上的先后顺序,可是需求同步,每个履行200ms;
  • 3.耗时、最不重要的使命,等一切使命履行结束后履行,每个履行需求1000ms。

优化前

/**
 * 优化前的写法, 这儿仅是演示模仿,实践的或许更杂乱
 */
private void doJobBeforeImprove(long startTime) {
    new TopPriorityJob(logger).doJob();
    for (int i = 0; i < 4; i++) {
        new SingleJob((i + 1), logger).doJob();
    }
    new LongTimeJob(logger).doJob();
    log("使命履行结束,一共耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}

履行成果:

因为一切的使命都是履行在主线程,串行履行,所以花了大约1865ms。

浅谈App的启动优化

优化后

/**
 * 优化后的写法, 这儿仅是演示模仿,实践的或许更杂乱
 */
private void doJobAfterImprove(final long startTime) {
    ConcurrentGroupTaskStep groupTaskStep = XTask.getConcurrentGroupTask();
    for (int i = 0; i < 4; i++) {
        groupTaskStep.addTask(buildSingleTask(i));
    }
    XTask.getTaskChain()
            .addTask(new MainInitTask(logger))
            .addTask(groupTaskStep)
            .addTask(new AsyncInitTask(logger))
            .setTaskChainCallback(new TaskChainCallbackAdapter() {
                @Override
                public void onTaskChainCompleted(@NonNull ITaskChainEngine engine, @NonNull ITaskResult result) {
                    log("使命彻底履行结束,一共耗时:" + (System.currentTimeMillis() - startTime) + "ms");
                }
            }).start();
    log("主线程使命履行结束,一共耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}
@NonNull
private XTaskStep buildSingleTask(int finalI) {
    return XTask.getTask(new TaskCommand() {
        @Override
        public void run() throws Exception {
            new SingleJob((finalI + 1), logger).doJob();
        }
    });
}    

履行成果:

因为只要优先级最重要的使命在主线程履行,其他使命都是异步履行,所以主线程使命履行耗费57ms,一切使命履行耗费1267ms。

浅谈App的启动优化

3.2 IO优化

3.网络恳求优化。

  • 发动进程中防止不必要的网络恳求。关于那些在发动时非必要履行的网络恳求,能够延时恳求或许运用缓存。
  • 关于需求进行多次串行网络恳求的接口进行优化整合,操控好恳求接口的粒度。比如后台有获取用户信息的接口、获取用户引荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。假如依次进行三次恳求,那么时刻根本上都花在网络传输上,尤其是在网络不稳定的状况下耗时尤为显着。但假如将这三个接口整合为获取用户的发动(初始化)信息,这样数据在网络中传输的时刻就会大大节约,一起也能进步接口的稳定性。

4.磁盘IO优化

  • 发动进程中防止不必要的磁盘IO操作。这儿的磁盘IO包含:文件读写、数据库(sqlite)读写和SharePreference等。
  • 关于发动进程中所有必要的数据加载,选择适宜的数据结构。能够选择支持随机读写、延时解析的数据存储结构以代替SharePreference。
  • 发动进程中防止很多的序列化和反序列化。

3.3 线程优化

咱们在开发运用的进程中,都或多或少会运用到线程。当咱们创立一个线程时,需求向体系申请资源,分配内存空间,这是一笔不小的开支,所以咱们平常开发的进程中都不会直接操作线程,而是选择运用线程池来履行使命。

但问题就在于假如线程池设置不对的话,很容易被人滥用,引发内存溢出的问题。而且一般一个运用会有多个线程池,不同功用、不同模块甚至是不同三方库都会有自己的线程池,这样咱们各用各的,就很难做到资源的和谐一致,劲不往一处使。

3.3.1 线程优化剖析

1.线程池耗时剖析。

要想进行线程优化,首要咱们就需求了解线程池在运用进程中,哪些地方比较耗时。

  • 首要,从常规上来讲,线程池在运用进程中,线程创立、线程切换和CPU调度比较耗时。
  • 其次,需求从事务的视点去剖析当前运用的线程运用状况,究竟是哪些使命导致线程池很多的创立和履行耗时。这儿咱们能够运用Hook的办法,去Hook线程的创立(ThreadFactory的newThread办法)和履行进行核算。
  • 最后,咱们需求结合事务的重要性(优先级)以及其相应的线程履行开支,进行归纳考量,调整线程池的履行战略。

2.线程池履行剖析。

然后再让咱们看看线程池的履行逻辑:

咱们知道,一个线程池一般由一个中心线程池和一个堵塞行列组成。那么当咱们调用线程池去履行一个使命的时分,线程池是怎么履行的呢?

中心线程池 -> 堵塞行列 -> 最大线程数(新建线程) -> RejectedExecutionHandler(回绝战略)
  • 当线程池中的中心线程池线程饱和后,使命会被塞进堵塞行列中等候。
  • 假如堵塞行列也满了,线程池会创立新的线程。
  • 可是假如现在线程池中的线程总数已经到达了最大线程数,这个时分会调用线程池的回绝战略(默许是直接中断,抛出反常)。

其中中心线程池的最大线程数并不是设置的越大越好,为什么这么说?因为CPU的处理才能是有限的,四核的CPU一次也只能一起履行四个使命,假如中心线程池数设置过大,那么各使命之间就会相互竞赛CPU资源,加大CPU的调度耗费,这样反而会下降线程池的履行功率。

一般来说,中心线程池的最大线程数满意下面的公式:

最佳中心线程数目 = ((线程等候时刻 + 线程CPU时刻)/ 线程CPU时刻 )* CPU数目

(1)线程等候时刻所占份额越高,需求越多线程。 (2)线程CPU时刻所占份额越高,需求越少线程。

这儿咱们考虑两种最常见的状况:

  • CPU密集型使命:这种使命绝大多数时刻都在进行CPU核算,线程等候时刻简直为0,因而这时最佳中心线程数为 n + 1(或许为n),这儿n为CPU中心数。
  • IO密集型使命:这种使命,CPU一般需求等候I/O(读/写)操作,这样CPU的处理时刻和等候时刻简直差不多,因而这时最佳中心线程数为 2n + 1(或许为2n),这儿n为CPU中心数。
3.3.2 线程优化方针

经过上面对线程池的剖析,咱们能够知道:

  • 线程池设置过大,会侵占内存,相互竞赛CPU资源,添加CPU的调度耗时。
  • 线程池设置过小,使命会堵塞串行,下降线程池履行功率。

因而,咱们线程优化的方针是:

(1)具有能够统筹大局的一致的线程池。 (2)能根据机器的功能来操控数量,合理分配线程池巨细。 (3)能够根据事务的优先级进行调度,优先级高的先履行。

3.3.3 线程优化详细办法

1.树立主线程池+副线程池的组合线程池,由线程池办理者一致和谐办理。主线程池担任优先级较高的使命,副线程池担任优先级不高以及被主线程池回绝降级下来的使命。

这儿履行的使命都需求设置优先级,使命优先级的调度经过PriorityBlockingQueue行列完成,以下是主副线程池的设置,仅供参阅:

  • 主线程池:中心线程数和最大线程数:2n(n为CPU中心数),60s keepTime,PriorityBlockingQueue(128)。
  • 副线程池:中心线程数和最大线程数:n(n为CPU中心数),60s keepTime,PriorityBlockingQueue(64)。

2.运用Hook的办法,搜集运用内所以运用newThread办法的地方,改为由线程池办理者一致和谐办理。

3.将一切供给了设置线程池接口的第三方库,经过其敞开的接口,设置为线程池办理者办理。没有供给设置接口的,考虑替换库或许插桩的办法,替换线程池的运用。

3.4 闪屏优化

闪屏优化归于发动用户体会的优化。究竟谁也不想运用页面一闪一闪的运用。

1.设置自定义闪屏页。

设置自定义的闪屏页能够进步咱们发动的”视觉速度”。一般会设置一个布景,然后把logo居中显现,能够运用xml文件来布局(留意,该图片不行展现动画,并且展现时刻也不行控)。这种办法能够给用户一种发动十分快的感觉,不仅处理了发动白屏的问题,并且展现了品牌logo也有助于提高品牌认知。

(1) 运用xml自定义一张带有logo的图片。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="?attr/xui_config_color_splash_bg"/>
    <item android:bottom="?attr/xui_config_app_logo_bottom">
        <bitmap
            android:gravity="center"
            android:src="?attr/xui_config_splash_app_logo"/>
    </item>
    <item android:bottom="?attr/xui_config_company_logo_bottom">
        <bitmap
            android:gravity="bottom"
            android:src="?attr/xui_config_splash_company_logo"/>
    </item>
</layer>

(2) 把这张图片经过设置主题的android:windowBackground特点办法显现为发动闪屏。

<style name="XUITheme.Launch.Demo">
    <item name="android:windowBackground">@drawable/xui_config_bg_splash</item>
    <item name="xui_config_splash_app_logo">@drawable/ic_splash_app_logo_xui</item>
    <item name="xui_config_splash_company_logo">@drawable/ic_splash_company_logo_xuexiang</item>
</style>

(3) 在manifest中将主页的主题设置为方才带发动图片的Launch主题。

<activity
    android:name=".activity.MainActivity"
    android:theme="@style/XUITheme.Launch.Demo">
</activity>

(4) 代码履行到主页面的onCreate的时分设置为程序正常的主题,这样就切回到正常主题布景了。

public class BaseActivity extends XPageActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
    }
}

2.运用Android12供给的发动画面Splash Screen。

developer.android.google.cn/guide/topic…

3.假如是因为某些不行抵抗的原因(例如一些大型游戏的首次加载),导致榜首次发动十分慢的话,能够在主页面进入之前,添加一个进度条加载页面。这样的优点便是要告知用户,究竟需求多久才能加载结束,给用户一个明确的信息,缓解用户的等候焦虑。

4.削减主页的跳转层次。除非某些不行抵抗的原因(比如广告作为一项重要收入来源),尽量不要设置发动页(SplashActivity),因为打开和封闭任何一个Activity都是需求耗费时刻的,每多一个Activity的跳转就意味着咱们主页面的打开时刻会被延伸。

3.5 主页面优化

主页面的发动和显现也是app发动十分重要的一部分。

3.5.1 布局优化

布局优化的中心便是:进步页面烘托的速度,防止页面过度烘托导致耗时。

  • 下降视图层级。削减冗余或许嵌套布局,防止页面过度烘托。合理运用merge标签和束缚布局ConstraintLayout
  • 不必要的布局推迟加载。用ViewStub代替在发动进程中不需求显现的UI控件。
  • 主页懒加载。主页不需求当即显现的页面,能够运用懒加载。
  • 运用自定义View代替杂乱的View叠加。

3.5.2 页面数据预加载

一般来说,咱们喜欢在每个页面内部才开端加载和显现数据,因为这样写或许更容易让人看懂,利于今后的保护。可是假如等页面UI布局初始化结束后,咱们才去加载数据的话,势必会添加页面发动显现的时刻。

因为每个页面(Activity)的发动自身便是比较耗时的进程,咱们能够将需求显现的数据进行预加载(即页面发动和数据加载一起进行,串行->并行),这样等页面UI布局初始化结束后,咱们就能够拿着预加载的数据直接烘托显现了,这样能够削减数据加载的等候,然后到达加速页面显现的意图。

这儿咱们能够参阅开源预加载库:PreLoader

浅谈App的启动优化

3.6 体系调度优化

  • 1.发动阶段不发动子进程,只在主进程履行Application的onCreate办法。因为子进程会同享CPU的资源,导致主进程CPU紧张。

  • 2.发动进程中削减体系调用,防止与AMSWMS竞赛锁。因为AMSWMS在运用发动的进程中承担了很多工作,且这些办法很多都是带锁的,这时运用应当防止与它们进行通讯,防止呈现很多的锁等候,堵塞要害操作。

  • 3.发动进程中除了Activity之外的组件发动要慎重。因为四大组件的发动都是经过主线程Handler进行驱动的,假如在运用发动的一起他们也发动,Handler中的Message势必会添加,然后影响运用的发动速度。

  • 4.发动进程中削减主线程Handler的运用。原因同上面相同,Activity的发动都是由主线程Handler进行驱动的,运用发动期间削减主线程Handler的运用,能够减小对主页面发动的影响。关于那些量大且频繁的使命调度,能够运用HandlerThread中的Looper创立归于子线程的handler来代替。

  • 5.运用IdleHandler。运用IdleHandler特性,在消息行列空闲时,对推迟使命进行分批初始化。

3.7 GC 优化

发动进程中应当削减GC的次数。因为GC会暂停程序的履行,然后会带来推迟的价值。那么咱们应当怎么防止频繁的GC呢?

  • 1.防止进行很多的字符串操作,特别是序列化和反序列化。不要运用+(加号)进行字符串拼接。
  • 2.防止临时目标的频繁创立,频繁创立的目标需求考虑复用。
  • 3.防止很多bitmap的绘制。
  • 4.防止在自定义View的onMeasureonLayoutonDraw中创立目标。

3.8 Webview发动优化

假如你的运用运用到了Webview,能够按需对Webview进行优化。

  • 1.因为WebView首次创立比较耗时,需求预先创立WebView,提早将其内核初始化。
  • 2.运用WebView缓存池,用到WebView的时分都从缓存池中拿。
  • 3.运用内置本地离线包,如一些字体和js脚本,即预置静态页面资源。

3.9 运用减肥

对运用减肥,能够最直接地加速资源加载的速度,然后进步运用发动的功率。

  • 1.删去没有引用的资源。咱们能够运用Inspect Code或许开启资源紧缩,主动删去无用的资源。
  • 2.能用xml写Drawable的,就不要运用UI切图。
  • 3.重用资源。同一图画的上色不同,咱们能够用android:tint和tintMode特点调整运用。
  • 4.紧缩png和jpeg文件。这儿引荐一个十分好用的图片紧缩插件:img-optimizer
  • 5.运用webp文件格局。Android Studio能够直接将现有的bmp,jpg,png或静态gif图画转换为webp格局。
  • 6.运用矢量图形,尤其是那些与分辨率无关,且可伸缩的小图标尽或许运用矢量图形。
  • 7.开启代码混杂。运用proGuard代码混杂器东西,包含紧缩、优化、混杂等功用。
  • 8.关于一些独立也非有必要的功用模块,选用插件化,按需加载。

3.10 资源重排

运用Linux的IO读取战略,PageCache和ReadAhead机制,依照读取顺序重新排列,削减磁盘IO次数。详细可拜见《支付宝 App 构建优化解析:经过安装包重排布优化 Android 端发动功能》 这篇文章。这种技能门槛较高,一般运用都不会用到。

3.11 类重排

类重排的完成经过ReDex的Interdex调整类在Dex中的排列顺序。调整Dex中类的顺序,把发动时需求加载的类按顺序放到主dex里。详细完成能够参阅《Redex初探与Interdex:Andorid冷发动优化》 这篇文章。

4. 怎么进行优化

上面讲了那么多运用发动优化的战略和办法,或许有些人就会问了:那么详细到咱们每个不同的项目上,咱们应该怎么进行优化呢?

以下是我个人的优化过程,仅供参阅:

  • 1.明确优化的内容和方针。首要,做任何优化一定是需求带着问题(意图)去优化的。任何不带意图进行的优化都是耍流氓。
  • 2.剖析现状、承认问题。当咱们现在需求优化的内容后,接下来便是需求进行很多的埋点核算、比较与剖析,承认究竟是因为什么原因导致的运用发动过慢,找到需求优化的部位。
  • 3.进行针对性的优化。找到导致运用发动过慢的问题之后,便是依照本篇讲述的优化战略和办法,进行针对性的优化。
  • 4.对优化成果进行总结并进行持续跟进。对优化前后的数据进行核算和比较,总结优化的经历并在组内进行共享,并在后续的版别中进行持续跟进。有条件的能够结合CI,添加线上的发动功能监控。

最后

讲了这么多,还是希望咱们在平常开发的进程中,多重视一些运用发动优化的相关技巧,这样等他人让你优化运用发动的时分,也就不会那么手足无措了。

我是xuexiangjys,一枚热爱学习,喜好编程,勤于思考,致力于Android架构研讨以及开源项目经历共享的技能up主。获取更多资讯,欢迎微信查找公众号:【我的Android开源之旅】

本文正在参加「金石方案 . 分割6万现金大奖」