Android性能优化(二)--更快--启动速度

1 App启动流程

  • App启动的整个过程,可以分解成下面几个过程:
  • 用户在Launcher上点击App Icon
  • 系统为App创建进程,显示启动窗口
  • App在进程中创建自己的组件

这个过程可以用下面这幅图来描述:

471cca892d17cb7d7e3bc02942c5e306.png

  • 我们能够优化的,也就是下面Application的创建部分。
  • 系统的进程分配以及一些窗口切换的动画效果等,都是跟ROM相关的,我们无法处理。
  • 系统在加载App的时候,,首先是加载了资源文件,这里就包括了要启动的Activity的Theme,可以定制很多东西,例如ActionBar、背景、StatBar等等。

2 启动时间的测量

2.1 关于Activity启动时间的定义

对于Activity来说,启动时,首先执行的是onCreate()、onStart()、onResume()这些生命周期函数,但即使这些生命周期方法回调结束了,应用也不算已经完全启动,还需要等View树全部构建完毕,一般认为,setContentView中的View全部显示结束了,算作是应用完全启动了。

2.2 Display Time

从API19之后,Android在系统Log中增加了Display的Log信息,通过过滤ActivityManager以及Display这两个关键字,可以找到系统中的这个Log:

$ adb logcat | grep “ActivityManager”
ActivityManager: Displayed com.example.launcher/.LauncherActivity: +999ms

抓到的Log如图所示:


3dace1b9c5fc7a9921f5.png

那么这个时间,实际上是Activity启动,到Layout全部显示的过程,但是要注意,这里并不包括数据的加载,因为很多App在加载时会使用懒加载模式,即数据拉取后,再刷新默认的UI。

2.3 reportFullyDrawn

前面说了,系统日志中的Display Time只是布局的显示时间,并不包括一些数据的懒加载等消耗的时间,所以,系统给我们定义了一个类似的『自定义上报时间』——reportFullyDrawn。

4dcaa273be05f32fdc3ffd8bc7ee6466.png

reportFullyDrawn是由我们自己调用的,一般在数据全部加载完毕后,手动调用,这样就会在Log中增加一条日志:

$ adb logcat | grep “ActivityManager”
ActivityManager: Displayed com.example.launcher/. LauncherActivity: +999ms
ActivityManager: Fully drawn com.example.launcher/. LauncherActivity: +1s999ms

使用的场景

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onLoadFinished(Loader loader, Void data) {
        // 加载数据
        // ……
        //但是要注意,这个方式需要API19+,所以,这里需要对SDK版本进行判断。
        // 上报reportFullyDrawn
        reportFullyDrawn();
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return null;
    }

    @Override
    public void onLoaderReset(Loader loader) {

    }
}

2.4 计算启动时间——ADB

通过ADB命令可以统计应用的启动时间,指令如下所示:

➜  ~  adb shell am start -W com.xys.preferencetest/.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xys.preferencetest/.MainActivity }
Status: ok
Activity: com.xys.preferencetest/.MainActivity
ThisTime: 1047
TotalTime: 1047
WaitTime: 1059
Complete
  • 该指令一共给出了三个时间:
  • 1.ThisTime:最后一个启动的Activity的启动耗时
  • 2.TotalTime:自己的所有Activity的启动耗时
  • 3.WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)
  • 这三个时间不是很好理解,我们可以把整个过程分解
  • 1.上一个Activity的onPause()——2.系统调用AMS耗时——3.第一个Activity(也许是闪屏页)启动耗时——4.第一个Activity的onPause()耗时——5.第二个Activity启动耗时
  • 那么,ThisTime表示5(最后一个Activity的启动耗时)。TotalTime表示3.4.5总共的耗时(如果启动时只有一个Activity,那么ThisTime与TotalTime应该是一样的)。WaitTime则表示所有的操作耗时,即1.2.3.4.5所有的耗时。
  • 每次给出的时间可能并不一样,而且应用从首次安装启动到后面每次正常启动,时间都会不同,区别于系统是否要分配进程空间。

2.5 计算启动时间——Screen Record

通过录屏进行启动的分析,是一个很好的办法,在API21+,Android给我们提供了一个更加方便、准确的方式:

➜  ~ adb shell screenrecord --bugreport /sdcard/test.mp4
  • Android在screenrecord中新增了一个参数——bugreport,那么加了这个参数之后,录制出来的视频,在左上角就会增加一行数字的显示,如图所示。
  • 在视频开始前,会显示设备信息和一些参数:
2fa1757a3fc7ef14235c.png
  • 视频开始后,左上角会有一行数字:
1e1f418cc0b6955fe309.png
  • 例如图中的:15:31:22.261 f=171(0)
  • 其中,前面的4个数字,就是时间戳,即15点31分22秒261,f=后面的数字是当前的帧数,注意,不是帧率,而是代表当前是第几帧,括号中的数字,代表的是『Dropped frames
    count』,即掉帧数。

3 优化点

  • Application是程序的主入口,特别是很多第三方SDK都会需要在Application的onCreate里面做很多初始化操作,不得不说,各种第三方SDK,都特别喜欢这个『兵家必争之地』,再加上自己的一些库的初始化,会让整个Application不堪重负。
  • 优化的方法,无非是通过以下几个方面
  • 1.延迟初始化
  • 2.后台任务
  • 3.界面预加载

3.1 延迟初始化

  • 延迟初始化并不是减少了启动时间,而是让耗时操作让位、让资源给UI绘制,将耗时的操作延迟到UI加载完毕后.
  • 我们的ContentView就是通过mDecoView.addView加入到根布局的,所以,通过这种方式,可以让延迟加载的内容,在ContentView初始化完毕后,再进行执行,保证了UI绘制的流畅性。
  • 所以,这里建议通过mDecoView.post方法,来进行延迟加载,代码如下:
getWindow().getDecorView().post(new Runnable() {

  @Override public void run() {
    ……
  }
});

3.2.后台任务

这个很简单,就是让App在onCreate里面尽可能的少做事情,而利用手机的多核特性,尽可能的利用多线程,例如一些第三方框架的初始化,如果能放线程,就尽量的放入线程中,最简单的,你可以直接new Thread(),当然,你也可以通过公共的线程池来进行异步的初始化工作,这个是最能够压缩启动时间的方式
IntentService

  • IntentService是继承于Service并处理异步请求的一个类,在IntentService的内部,有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,而不需要去手动控制。
public class InitIntentService extends IntentService {

    private static final String ACTION = "com.xys.startperformancedemo.action";

    public InitIntentService() {
        super("InitIntentService");
    }

    public static void start(Context context) {
        Intent intent = new Intent(context, InitIntentService.class);
        intent.setAction(ACTION);
        context.startService(intent);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        SystemClock.sleep(2000);
        Log.d(TAG, "onHandleIntent: ");
    }
}
  • 我们将耗时任务丢到IntentService中去处理,系统会自动开启线程去处理,同时,在任务结束后,还能自己结束Service,多么的人性化!OK,只需要在Application或者Activity的onCreate中去启动这个IntentService即可:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    InitIntentService.start(this);
}

最后不要忘记在Mainifest注册Service。

3.3 界面预加载

  • 当系统加载一个Activity的时候,onCreate()是一个耗时过程,那么在这个过程中,系统为了让用户能有一个比较好的体验,实际上会先绘制一些初始界面,类似于PlaceHolder
  • 系统首先会读取当前Activity的Theme,然后根据Theme中的配置来绘制,当Activity加载完毕后,才会替换为真正的界面。所以,Google官方提供的解决方案,就是通过android:windowBackground属性,来进行加载前的配置,同时,这里不仅可以配置颜色,还能配置图片,例如,我们可以使用一个layer-list来作为android:windowBackground要显示的图:
  • 配置中的android:opacity="opaque"参数是为了防止在启动的时候出现背景的闪烁。
    **此处缺代码
  • 这里需要注意下,一定是Activity的Theme,而不是Application的Theme。
  • 最后,我们在Activity加载真正的界面之前,将Theme设置回正常的Theme就好了:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
        SystemClock.sleep(2000);
        setContentView(R.layout.activity_main);
    }
}
  • 在这个Activity中,我使用SystemClock.sleep(2000),模拟了一个Activity加载的耗时过程,在super.onCreate(savedInstanceState)调用前,将主题重新设置为原来的主题。
  • 启动的时候,会先展示一个画面,这个画面就是系统解析到的Style,等Activity加载完全完毕后,才会加载Activity的界面,而在Activity的界面中,我们将主题重新设置为正常的主题,从而达到一个友好的启动体验,这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。

3.4 使用ActivityLifecycleCallbacks

  • Framework提供的这个方法可以监控到所有Activity的生命周期,在这里,我们就可以通过onActivityCreated这样一个回调,来将一些UI相关的初始化操作放到这里,同时,通过unregisterActivityLifecycleCallbacks来避免重复的初始化。
  • 同时,这里onActivityCreated回调的参数Bundle,可以用来区别是否是被系统所回收的Activity。
public class MainApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化基本内容
        // ……
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                unregisterActivityLifecycleCallbacks(this);
                // 初始化UI相关的内容
                // ……
            }

            @Override
            public void onActivityStarted(Activity activity) {
            }

            @Override
            public void onActivityResumed(Activity activity) {
            }

            @Override
            public void onActivityPaused(Activity activity) {
            }

            @Override
            public void onActivityStopped(Activity activity) {
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }

            @Override
            public void onActivityDestroyed(Activity activity) {
            }
        });
    }
}

3.5 Multidex

  • 对应5.0以下的65535问题,目前只能通过Multidex来进行处理,而在5.0以下的机器上,系统在加载前的合并Dex的过程,有可能非常长

参考

Android启动速度优化
一触即发 App 启动优化最佳实践

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

推荐阅读更多精彩内容