NO IMAGE

一: App啟動方式

通常來說, 一個App啟動會分如下不同的狀態:

    1.  冷啟動
    App沒有啟動過或App程序被killed, 總之是系統中不存在該App程序, 此時啟動App即為冷啟動。

  • 冷啟動的流程即是App啟動流程的全過程, 需要建立App程序, 載入相關資源, 啟動Main Thread, 初始化首屏Activity等.

  • 在這個過程中, 螢幕會顯示一個空白的視窗(顏色基於主題), 直至首屏Activity完全啟動.

  • 下圖展示了冷啟動的時間線:

     

    2. 熱啟動

  • 當啟動應用時,後臺已有該應用的程序,比如按下home鍵,這種在已有程序的情況下,這種啟動會從已有的程序中來啟動應用,這種啟動方式叫熱啟動, 系統只是將其從後臺帶到前臺, 展示給使用者.

    類同與冷啟動, 在這個過程中, 螢幕會顯示一個空白的視窗(顏色基於主題), 直至activity渲染完畢.

  • 溫啟動
    介於冷啟動和熱啟動之間, 一般來說在以下兩種情況下發生:

    • 使用者back退出了App, 然後又啟動. App程序可能還在執行, 但是activity需要重建.
    • 使用者退出App後, 系統可能由於記憶體原因將App殺死, 程序和activity都需要重啟, 但是可以在onCreate中將被動殺死鎖儲存的狀態(saved instance state)恢復.

通過三種啟動狀態的相關描述, 可以看出我們要做的啟動優化其實就是針對冷啟動. 熱啟動和溫啟動都相對較快.

二. 哪些地方是App快速啟動的敵人

根據冷啟動的時間圖, 可以看出, 對於App來說, 我們可以控制的啟動時間線的點無外乎:

  • Application的onCreate
  • 首屏Activity的渲染

而我們現在的App動不動整合了很多第三方服務, 啟動時需要檢查廣告, 註冊狀態等等一系列介面都是在Application的onCreate或是首屏的onCreate中做的.

三:分析工具Traceview

3.1 Traceview介紹

Traceview是一個效能分析工具, 主要是分析當前執行緒情況, 各個方法執行時間等. 如下:

traceview

指標說明:

  • Incl(Inclusive) Cpu Time
    方法本身和其呼叫的所有子方法佔用CPU時間.
  • Excl(Exclusive) Cpu Time
    方法本身佔用CPU時間.
  • Incl Real Time
    方法(包含子方法)開始到結束用時.
  • Excl Real Time
    方法本身開始到結束用時.
  • Call Recursion Calls/Total
    方法被呼叫次數 方法被遞迴呼叫次數.
  • Cpu Time/Call
    方法呼叫一次佔用CPU時間.
  • Real Time/Call
    方法呼叫一次實際執行時間.

一般來說, 我們使用Real Time/Call排序來找出耗時多的方法

有必要解釋下CPU Time和Real Time:
CPU Time 方法實際執行時間(不包括io等待時間)
Real Time 方法開始結束時間差(包括等待時間)

3.2 Traceview使用

有兩種方式來使用Traceview:
1. 通過DDMS:

start traceview

這種方式啟動再結束後介面會自動彈出分析介面

點選開始時會彈出一個選擇trace模式的框, 預設選中”Sample based profiling”即可:

traceview option

  • Sample based profiling(基於樣本分析)
    根據取樣時間間隔來規律的打斷VM來記錄方法呼叫棧(Call Stack), 開銷和取樣頻率成比例.

  • Trace based profiling(基於完整trace資料分析)
    記錄每個方法的出入口, 每個方法執行時都開啟記錄, 無論多小的方法, 因此開銷很大.

2. 使用程式碼:


// 在自己想要開始除錯的地方start
Debug.startMethodTracing("GithubApp");
// 在合適的地方stop
Debug.stopMethodTracing();

注: 以上方法開啟trace的方式相當於”Trace based profiling”, 會記錄每個方法的執行. Android 4.4及以上可以呼叫startMethodTracingSampling()來用程式碼開啟”Sample based profiling”的trace方式.

再需要統計時間的程式碼前後新增上面的程式碼

執行程式, 會在sdcard上生成一個”GithubApp.trace”的檔案.

注意: 需要給程式加上寫儲存的許可權:


<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

通過adb pull將其匯出到本地


adb pull /sdcard/GithubApp.trace ~/temp

開啟DDMS分析trace檔案

ddms_open_trace

分析trace檔案

traceview_ui

  1. 在下方的方法區點選”Real Time/Call”, 按照方法每次呼叫耗時降序排.
  2. 耗時超過500ms都是值得注意的.
  3. 點選每個方法, 可以看到其父方法(呼叫它的)和它的所有子方法(它呼叫的).
  4. 點選方法時, 上方的該方法執行時間軸會閃動, 可以看該方法的執行執行緒及相對時長.

四: 應用的啟動時間統計

adb shell am start -W PackageName/PackageName.MainActivity

執行成功後將返回三個測量到的時間:


1、ThisTime:一般和TotalTime時間一樣,除非在應用啟動時開了一個透明的Activity預先處理一些事再顯示出主Activity,這樣將比TotalTime小。 
2、TotalTime:應用的啟動時間,包括建立程序 Application初始化 Activity初始化到介面顯示。 
3、WaitTime:一般比TotalTime大點,包括系統影響的耗時。

五: 程式碼分析

因為這個App整合了Bugly, Push, Feedback等服務, 所以Application的onCreate有很多第三方平臺的初始化工作…


public class GithubApplication extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
// init logger.
AppLog.init();
// init crash helper
CrashHelper.init(this);
// init Push
PushPlatform.init(this);
// init Feedback
FeedbackPlatform.init(this);
// init Share
SharePlatform.init(this);
// init Drawer image loader
DrawerImageLoader.init(new AbstractDrawerImageLoader() {
@Override
public void set(ImageView imageView, Uri uri, Drawable placeholder) {
ImageLoader.loadWithCircle(GithubApplication.this, uri, imageView);
}
});
}
}

當前冷啟動效果:

 

code_start_before_optimize

可以看到啟動時白屏了很長時間.

通過在onCreate方法前後新增日誌,用traceview進行分析啟動速度

看左邊的方法名, 可以看到耗時大戶就是我們呼叫的第三方SDK的初始化方法, 特別是Bugly, 還載入native的lib, 用ZipFile操作等.

調整Application onCreate再試

既然已經知道了哪些地方耗時長, 我們不妨調整下Application的onCreate實現, 一般來說我們可以將這些初始化放在一個單獨的執行緒中處理, 為了方便今後管理, 這裡我用了一個InitializeService的IntentService來做初始化工作.

明確一點, IntentService不同於Service, 它是工作在後臺執行緒的.

InitializeService.java程式碼如下:


package com.anly.githubapp.compz.service;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.widget.ImageView;
import com.anly.githubapp.common.wrapper.AppLog;
import com.anly.githubapp.common.wrapper.CrashHelper;
import com.anly.githubapp.common.wrapper.FeedbackPlatform;
import com.anly.githubapp.common.wrapper.ImageLoader;
import com.anly.githubapp.common.wrapper.PushPlatform;
import com.anly.githubapp.common.wrapper.SharePlatform;
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
import com.mikepenz.materialdrawer.util.DrawerImageLoader;
/**
* Created by mingjun on 16/8/25.
*/
public class InitializeService extends IntentService {
private static final String ACTION_INIT_WHEN_APP_CREATE = "com.anly.githubapp.service.action.INIT";
public InitializeService() {
super("InitializeService");
}
public static void start(Context context) {
Intent intent = new Intent(context, InitializeService.class);
intent.setAction(ACTION_INIT_WHEN_APP_CREATE);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_INIT_WHEN_APP_CREATE.equals(action)) {
performInit();
}
}
}
private void performInit() {
AppLog.d("performInit begin:"   System.currentTimeMillis());
// init Drawer image loader
DrawerImageLoader.init(new AbstractDrawerImageLoader() {
@Override
public void set(ImageView imageView, Uri uri, Drawable placeholder) {
ImageLoader.loadWithCircle(getApplicationContext(), uri, imageView);
}
});
// init crash helper
CrashHelper.init(this.getApplicationContext());
// init Push
PushPlatform.init(this.getApplicationContext());
// init Feedback
FeedbackPlatform.init(this.getApplication());
// init Share
SharePlatform.init(this.getApplicationContext());
AppLog.d("performInit end:"   System.currentTimeMillis());
}
}

GithubApplication的onCreate改成:


public class GithubApplication extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
// init logger.
AppLog.init();
InitializeService.start(this);
}
}

看看現在的效果:

可以看到提升了很多, 但是啟動的時候會有一個白屏, 如果手機較慢的話, 這個白屏就會持續一段時間

給我們的應用視窗弄一個PlaceHolder

Android最新的Material Design有這麼個建議的. 建議我們使用一個placeholder UI來展示給使用者直至App載入完畢.

怎麼做呢?

給Window加上背景

 當App沒有完全起來時, 螢幕會一直顯示一塊空白的視窗(一般來說是黑屏或者白屏, 根據App主題).

這個空白的視窗展示跟主題相關, 那麼我們是不是可以從首屏的主題入手呢? 恰好有一個windowBackground的主題屬性, 我們來給Splash介面加上一個主題, 帶上我們想要展示的背景.

做一個logo_splash的背景:


<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 底層白色 -->
<item android:drawable="@color/white" />
<!-- 頂層Logo居中 -->
<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_github" />
</item>
</layer-list>

弄一個主題:


<style name="SplashTheme" parent="AppTheme">
<item name="android:windowBackground">@drawable/logo_splash</item>
</style>

將一個什麼不渲染布局的Activity作為啟動屏

寫一個什麼都不做的LogoSplashActivity.


public class LogoSplashActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 注意, 這裡並沒有setContentView, 單純只是用來跳轉到相應的Activity.
// 目的是減少首屏渲染
if (AppPref.isFirstRunning(this)) {
IntroduceActivity.launch(this);
}
else {
MainActivity.launch(this);
}
finish();
}
}

在AndroidManifest.xml中設定其為啟動屏, 並加上主題:


<activity
android:name=".ui.module.main.LogoSplashActivity"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

5.最終的效果

讓我們來看下最終的效果:

 

相比之前, 呈現給使用者的不再是一個白屏了, 帶上了logo, 當然這個背景要顯示什麼, 我們可以根據實際情況來自定義.

這種優化, 對於有些Application內的初始化工作不能移到子執行緒做的情況, 是非常友好的. 可以避免我們的App長時間的呈現給使用者一個空白的視窗.