Android App启动优化

目录:

一、App启动方式

二、App启动时间度量

三、启动优化辅助工具

四、优雅获取方法耗时

五、App启动速度优化


一、App启动方式

冷启动:App启动前,该App进程还没有创建,例如在安装后的第一次启动、设备重启或者应用被杀死情况下发生。

热启动:当App启动时,后台已经有该App的进程,例如按Home键退出App。热启动因为会从已有的进程中来启动,不会再走Application的初始化了。

温启动:介于冷启动和温启动之间,有许多潜在状态可视为温启动。例如

  • 用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。

  • 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存的实例
    state bundle 对于完成此任务有一定助益。

这个温启动的概念有些文章会有,但是并不重要,因为我们优化App启动速度是针对冷启动而言的。

这三种概念详细请看官方文档 https://developer.android.google.cn/topic/performance/vitals/launch-time#cold

二、App启动时间度量

1、adb shell方式:
通过adb命令执行am命令启动App并出界应用启动时间。

命令格式:

adb shell am start -W packagename/packagename.首页Activity

示例:

adb shell am start -W com.example.demoapp/com.example.demoapp.MainActivity2

结果:

➜ DemoApp adb shell am start -W com.example.demoapp/com.example.demoapp.MainActivity2
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.demoapp/.MainActivity2 }
Status: ok
Activity: com.example.demoapp/.MainActivity2
ThisTime: 967
TotalTime: 967
WaitTime: 1011
Complete

ThisTime:表示一连串启动 Activity 的最后一个 Activity 的启动耗时;

TotalTime:表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用Activity pause的耗时。

WaitTime:返回从 startActivity 到应用第一帧完全显示这段时间. 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;

2.adb logcat 方式
Android 4.4之后,Android在系统Log中添加了Display的log信息,可以通过过滤ActivityManager(或者是ActivityTaskManager,跟android版本有关,源码做了改动)关键字,抓取logcat 中的启动时间信息。注意这里的时间不包括数据的加载,因为很多应用在加载时会启动懒加载模式,技术局获取会后再刷新显示UI,所以如果需要获取全部时间包括数据加载时间,需要在你的activity代码的数据加载完的回调函数中加上 reportFullDrawn()。最终抓的logcat 的信息会如下图所示。

1651665689(1).png

3.手动打点计时
启动时埋点,启动结束埋点,二者差值
建一个 LaunchTimer 类来记录APP启动和结束的时间差

import android.util.Log;

public class LauncherTimer {

    private static long time;

    public static void startRecord() {
        time = System.currentTimeMillis();
    }

    public static void endRecord(String msg) {
        long cost = System.currentTimeMillis() - time;
        Log.d("LauncherTimer", msg + " cost time = " + cost);
    }

}

在 Application 的 attachBaseContext(attachBaseContext 是应用启动过程中我们所能接触到第一个方法) 方法中记录启动开始时间,如下所示

import android.app.Application;
import android.content.Context;
import com.mumuxi.testapplication.android.utils.LauncherTimer;

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        LauncherTimer.startRecord();
    }

    @Override
    public void onCreate() {
        super.onCreate();   
    }


}

在记录APP启动结束时间是可以在 onWindowFocusChanged 方法中记录。onWindowFocusChanged是Activit的首帧时间,是activity首次绘制的时间。不过如果有时候有重要视图需要数据延迟加载后再显示的话,可以选择在数据加载后,第一次显示UI时再记录APP启动结束时间。毕竟我们是为了改善用户的体验,并不是单纯的为了把启动时间缩短,数据延迟加载导致的重要视图未显示这一点也在我们的优化范围内。

如果是列表展示的情况,我们可以选择在真实数据展示,Feed 第一条展示出来,记录启动结束时间。如下所示:

public class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
    /**
     * 是否已经统计过
     */
    private boolean mHasRecorded;

    @Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
        //onBindViewHolder 回调多次,而只统计一次,加个变量标识
        if (position == 0 && !mHasRecorded) {
            mHasRecorded = true;
            holder.layout.getViewTreeObserver()
                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            holder.layout.getViewTreeObserver().removeOnPreDrawListener(this);
                            LauncherTimer.endRecord("FeedShow");
                            return true;
                        }
                    });
        }
    }
}

三、启动优化工具

1.Android Studio CPU Profile(推荐使用,方便快捷)

第一步:如图点击选择Edit Configurations


第二步:如图选择:(第一步后弹出如下框进行相关选择)


相关解释:Java/Kotlin Method Sample :对Java方法进行采样有可能某个方法采集不到,所以选择 Java/Kotlin Method Trace(跟踪采样)

第三步:如图选择Profile app运行


第四步 :App已经运行到我们想要的效果后,点击stop, 运行结束 如图



第五步进行分析:
详细请参考 Android Studio CPU profiler性能分析工具介绍和使用详解

四、优雅获取方法耗时

我们在做启动优化的时候通常需要知道启动阶段所有方法的耗时,这样可以有针对性的分析出耗时较多的方法。通过一些工具我们是可以查看到方法的耗时,但是通过工具我们无法做到直观的两组优化前后的各方法耗时的数据对比。启动优化工具只是帮助我们看到哪些方法耗时,整个调用链是怎么样的,这样我们就能从中找到优化点在哪里。找到优化点后,我们再自动记录优化前后的各个方法的耗时,保存起来导出excel 来直观地对比。

对于一般的获取方法的耗时的方式是通过手动埋点来实现,然后我们可以把这些数据记录到文件中,然后再做对比。比如在某个方法开始和结束的位置分别插入以下代码:

long time = System.currentTimeMillis();
initJpush();
long cost = System.currentTimeMillis() - time;

当有多个方法需要埋点时,同理这样写就可以获取到每个方法的执行时间了,但是这样操作存在的问题也是显而易见的,当然我相信你肯定也发现了,主要总结为以下几点:

  • 代码重复、耦合度高并且看起来非常恶心
  • 侵入性强
  • 工作量大

那么针对这种方式的劣势,如何才能更加优雅的实现获取方法的耗时呢?答案就是采用AOP的方式来实现。AOP 方案的实现,有兴趣请自行了解
谈谈Android AOP技术方案
Android AOP — AspectJ的使用

五、App启动速度优化

优化总方针:

  • 懒加载、延迟加载、异步加载,梳理业务选择对应技术。
  • 优化体验,设计启动屏幕页改善启动体验,同时在这过程中实现对一些网络数据、数据库文件的异步预加载。
  • 技术上的优化。

1.懒加载:
核心思想:需要用到时才加载。下面列具一些:

  • 布局懒加载 ,不必在启动时展示的view 可以 通过ViewStub实现。
  • ViewPager 懒加载
  • 一些重量级的对象的初始化,也实现懒加载。

2.延迟初始化
核心思想:不阻碍启动过程,找到合适的时机再去加载。
难点:对于合适的加载时机难以把控。

  • 如Handler 的 postDelayed这种延迟一个时间来加载(不建议使用,设置的时间难以把控,后续项目的迭代更新也可能导致这个时间需要修改,难以把握,如果不把这个时间的设置逻辑写出来的话,可读性差,难以维护)

  • ui展示第一帧后开始加载。在Activity的onWindowFocusChange()方法第一次回调后执行。

  • 数据列表展示后。Feed 第一条数据后执行。缺点:用户可能在滑动列表,可能会导致卡顿。

  • 利用MessageQueue 的 空闲任务 IdleHandler 机制 来实现延迟加载。

推荐此方案,执行时机明确,执行的时机是在系统空闲的时候进行执行,有效缓解列表卡顿,它可以真正的提升用户的体验,不过需要注意queueIdle方法回调的执行也是会阻塞线程的,如果任务耗时比较长,可以选择建立任务队列,一个一个任务地执行。任务队列过长,可能会导致任务不执行或者执行时间过长,因为queueIdle的回调是MessageQueue 的 next()第一次轮训时,message 为空才执行一次。详细请看 MessageQueue 空闲任务 IdleHandler 机制

/*** 延迟初始化分发器 */
public class DelayInitDispatcher {

    private Queue<Runnable> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if (mDelayTasks.size() > 0) {
                mDelayTasks.poll().run();
            }
            return !mDelayTasks.isEmpty();
        }
    };

    public DelayInitDispatcher addTask(Runnable task) {
        mDelayTasks.add(task);
        return this;
    }

    public void start() {
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}

3.异步加载
核心思想:子线程分担主线程任务,并行减少时间。
难点:说到子线程,那大家想到的肯定是线程池了,但是如果任务间存在依赖关系,我们就无法简单地直接把任务添加到线程池就能完成。即使你把有依赖关系的任务,合并到同一个任务去,在可读性上也比较差。所以有以下方案的推荐:

阿里开源的一个启动器库alpha:https://github.com/alibaba/alpha

启动器介绍:
核心思想:充分利用CPU多核,自动梳理任务顺序

启动器流程:

  • 代码Task化,启动逻辑抽象为Task
  • 根据所有任务依赖关系排序生成一个有向无环图(自动生成的)
  • 多线程按照排序后的优先级依次执行

4.优化体验实现预加载

方案:
主要是从交互设计上来优化,例如刚启动app时,可以设置 SplashActivity,可以设置欢迎页、展示App logo。进入SplashActivity,大约经过1~2秒再跳转到程序的主界面。并且我们在可以在SplashActivity 的 onWindowFocusChange()方法回调后做一些其他初始化工作,达到预加载的效果。目前很多App都是这么做的。当然也可以直接在拉起MainActivity后先展示logo 、欢迎页等,等待1-2秒再展示真正的界面,并在onWindowFocusChange()方法回调后做一些其他初始化工作,达到预加载的效果。

https://blog.csdn.net/yywan1314520/article/details/51622154

5.技术优化

  • xml 布局优化

  • 启动阶段不要启动子进程,子进程会共享cpu资源,导致主进程cpu紧张

  • 避免在Application的构造函数、attachBaseContext()、onCreate()内做耗时操作;一些数据预取放在异步线程执行。

  • ContentProvider 的 onCreate()内不要做耗时操作,Application onCreate 之前是ContentProvider(顺序为attachBaseContext -> ContentProvider -> onCreate )。对于只是为了自动初始化而利用ContentProvider 的情况,使用Jetpack 的 Startup 也可以实现启动优化。详细请看下面的文章:
    Jetpack新成员,App Startup一篇就懂

  • Activity 的onCreate()、onResume()内不要做耗时操作,ui显示第一帧后会回调onWindowFocusChange方法,可以考虑在这里做一些初始化工作。

  • 避免在启动阶段导致进程GC,后台任务影响启动速度中还有还有另一个比较典型的 case 就是 GC,触发 GC 后可能会抢占我们的 cpu 资源甚至导致我们的线程被挂起,如果启动过程中存在大量的 GC,那么我们的启动速度将会受到比较大的影响。

  • Mutidex.install()优化(如果已经不考虑低版本Android5.0以下机器,可忽略)
    Multidex优化Demo地址
    抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减少80%

一些启动优化黑科技简单介绍,上网搜索相关字段可以搜索到一些文章,这里没有成熟的方案提供给大家:

  • 启动阶段抑制GC
  • CPU锁频
  • 类加载优化
  • IO优化
  • 安装包重排布

参考:
抖音 Android 性能优化系列:启动优化实践
性能优化总结-Android启动速度优化
Android启动优化你真的了解吗?
深入探索Android启动速度优化(上)
深入探索Android启动速度优化(下)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,273评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,349评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,709评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,520评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,515评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,158评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,755评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,660评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,203评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,287评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,427评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,122评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,801评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,272评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,393评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,808评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,440评论 2 359

推荐阅读更多精彩内容

  • 前言 本文是本人学习Android app 启动优化的总结,由于本人知识有限,难免有错误或疏漏的地方,请大家多多指...
    取了个很好听的名字阅读 1,311评论 0 1
  • App启动分为冷启动和热启动 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应...
    wenou阅读 348评论 2 1
  • 用户希望应用能够及时响应并快速加载。启动时间过长的应用可能会导致用户在对应用给出很低的评分,甚至完全弃用。 启动状...
    zcwfeng阅读 3,021评论 1 28
  • title: Android app 启动优化date: 2017-03-12 18:44:12tags: App...
    苏州丸子阅读 1,190评论 0 5
  • 知其然知其所以然 凡是不知道的事情,查文档,肯定能查出个究竟了解启动时,系统和应用程序发生了什么,以及他们在这些状...
    Natchi阅读 765评论 0 0