在性能優化的整個知識體系中,最重要的就是穩定性優化,在上一篇文章 《深刻探索Android穩定性優化》 中咱們已經深刻探索了Android穩定性優化的疆域。那麼,除了穩定性之外,對於性能緯度來講,哪一個方面的性能是最重要的呢?毫無疑問,就是應用的啓動速度。下面,就讓咱們揚起航帆,一塊兒來逐步深刻探索Android啓動速度優化的奧祕。html
若是咱們去一家餐廳吃飯,在點餐的時候等了半天都沒有服務人員過來,可能就沒有耐心等待直接走了。java
對於App來講,也是一樣如此,若是用戶點擊App後,App半天都打不開,用戶就可能失去耐心卸載應用。python
啓動速度是用戶對咱們App的第一體驗,打開應用後才能去使用其中提供的強大功能,就算咱們應用的內部界面設計的再精美,功能再強大,若是啓動速度過慢,用戶第一印象就會不好。linux
所以,拯救App的啓動速度,迫在眉睫。android
應用啓動的類型總共分爲以下三種:git
下面,咱們來詳細分析下各個啓動類型的特色及流程。github
從點擊應用圖標到UI界面徹底顯示且用戶可操做的所有過程。web
耗時最多,衡量標準。算法
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImplshell
首先,用戶進行了一個點擊操做,這個點擊事件它會觸發一個IPC的操做,以後便會執行到Process的start方法中,這個方法是用於進程建立的,接着,便會執行到ActivityThread的main方法,這個方法能夠看作是咱們單個App進程的入口,至關於Java進程的main方法,在其中會執行消息循環的建立與主線程Handler的建立,建立完成以後,就會執行到 bindApplication 方法,在這裏使用了反射去建立 Application以及調用了 Application相關的生命週期,Application結束以後,便會執行Activity的生命週期,在Activity生命週期結束以後,最後,就會執行到 ViewRootImpl,這時纔會進行真正的一個頁面的繪製。
直接從後臺切換到前臺。
啓動速度最快。
只會重走Activity的生命週期,而不會重走進程的建立,Application的建立與生命週期等。
較快,介於冷啓動和熱啓動之間的一個速度。
LifeCycle -> ViewRootImpl
它是GUI管理系統與GUI呈現系統之間的橋樑。每個ViewRootImpl關聯一個Window, ViewRootImpl 最終會經過它的setView方法綁定Window所對應的View,並經過其performTraversals方法對View進行佈局、測量和繪製。
須要注意的是,這些都是系統的行爲,通常狀況下咱們是沒法直接干預的。
一般到了界面首幀繪製完成後,咱們就能夠認爲啓動已經結束了。
咱們的優化方向就是 Application和Activity的生命週期 這個階段,由於這個階段的時機對於咱們來講是可控的。
在Android Studio Logcat中過濾關鍵字「Displayed」,能夠看到對應的冷啓動耗時日誌。
使用adb shell獲取應用的啓動時間
// 其中的AppstartActivity全路徑能夠省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路徑]
複製代碼
執行後會獲得三個時間:ThisTime、TotalTime和WaitTime,詳情以下:
表示最後一個Activity啓動耗時。
表示全部Activity啓動耗時。
表示AMS啓動Activity的總耗時。
通常來講,只需查看獲得的TotalTime,即應用的啓動時間,其包括 建立進程 + Application初始化 + Activity初始化到界面顯示 的過程。
能夠寫一個統計耗時的工具類來記錄整個過程的耗時狀況。其中須要注意的有:
其代碼以下所示:
/**
* 耗時監視器對象,記錄整個過程的耗時狀況,能夠用在不少須要統計的地方,好比Activity的啓動耗時和Fragment的啓動耗時。
*/
public class TimeMonitor {
private final String TAG = TimeMonitor.class.getSimpleName();
private int mMonitord = -1;
// 保存一個耗時統計模塊的各類耗時,tag對應某一個階段的時間
private HashMap<String, Long> mTimeTag = new HashMap<>();
private long mStartTime = 0;
public TimeMonitor(int mMonitorId) {
Log.d(TAG, "init TimeMonitor id: " + mMonitorId);
this.mMonitorId = mMonitorId;
}
public int getMonitorId() {
return mMonitorId;
}
public void startMonitor() {
// 每次從新啓動都把前面的數據清除,避免統計錯誤的數據
if (mTimeTag.size() > 0) {
mTimeTag.clear();
}
mStartTime = System.currentTimeMillis();
}
/**
* 每打一次點,記錄某個tag的耗時
*/
public void recordingTimeTag(String tag) {
// 若保存過相同的tag,先清除
if (mTimeTag.get(tag) != null) {
mTimeTag.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
Log.d(TAG, tag + ": " + time);
mTimeTag.put(tag, time);
}
public void end(String tag, boolean writeLog) {
recordingTimeTag(tag);
end(writeLog);
}
public void end(boolean writeLog) {
if (writeLog) {
//寫入到本地文件
}
}
public HashMap<String, Long> getTimeTags() {
return mTimeTag;
}
}
複製代碼
爲了使代碼更好管理,咱們須要定義一個打點配置類,以下所示:
/**
* 打點配置類,用於統計各階段的耗時,便於代碼的維護和管理。
*/
public final class TimeMonitorConfig {
// 應用啓動耗時
public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}
複製代碼
此外,耗時統計可能會在多個模塊和類中須要打點,因此須要一個單例類來管理各個耗時統計的數據:
/**
* 採用單例管理各個耗時統計的數據。
*/
public class TimeMonitorManager {
private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;
public synchronized static TimeMonitorManager getInstance() {
if (mTimeMonitorManager == null) {
mTimeMonitorManager = new TimeMonitorManager();
}
return mTimeMonitorManager;
}
public TimeMonitorManager() {
this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
}
/**
* 初始化打點模塊
*/
public void resetTimeMonitor(int id) {
if (mTimeMonitorMap.get(id) != null) {
mTimeMonitorMap.remove(id);
}
getTimeMonitor(id).startMonitor();
}
/**
* 獲取打點器
*/
public TimeMonitor getTimeMonitor(int id) {
TimeMonitor monitor = mTimeMonitorMap.get(id);
if (monitor == null) {
monitor = new TimeMonitor(id);
mTimeMonitorMap.put(id, monitor);
}
return monitor;
}
}
複製代碼
主要在如下幾個方面須要打點:
例如,啓動時在Application和第一個Activity加入打點統計:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("Application-onCreate");
}
複製代碼
@Override
protected void onCreate(Bundle savedInstanceState) {
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate");
super.onCreate(savedInstanceState);
initData();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate-Over");
}
@Override
protected void onStart() {
super.onStart();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("SplashActivity-onStart", false);
}
複製代碼
精確,可帶到線上,可是代碼有侵入性,修改爲本高。
一、在上傳數據到服務器時建議根據用戶ID的尾號來抽樣上報。
二、onWindowFocusChanged只是首幀時間,App啓動完成的結束點應該是真實數據展現出來的時候(一般來講都是首幀數據),如列表第一條數據展現,記得使用getViewTreeObserver().addOnPreDrawListener()(在API 16以上能夠使用addOnDrawListener),它會把任務延遲到列表顯示後再執行,例如,在Awesome-WanAndroid項目的主頁就有一個RecyclerView實現的列表,啓動結束的時間就是列表的首幀時間,也即列表第一條數據展現的時候。這裏,咱們直接在RecyclerView的適配器ArticleListAdapter的convert(onBindViewHolder)方法中加上以下代碼便可:
if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
mHasRecorded = true;
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
LogHelper.i("FeedShow");
return true;
}
});
}
複製代碼
具體的實例代碼可在 這裏查看。
由於用戶看到真實的界面是須要有網絡請求返回真實數據的,可是onWindowFocusChanged只是界面繪製的首幀時機,可是列表中的數據是須要從網絡中下載獲得的,因此應該以列表的首幀數據做爲啓動結束點。
面向切面編程,經過預編譯和運行期動態代理實現程序功能統一維護的一種技術。
利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合性下降,提升程序的可重用性,同時大大提升了開發效率。
對哪些方法進行攔截,攔截後怎麼處理。
類是對物體特徵的抽象,切面就是對橫切關注點的抽象。
被攔截到的點(方法、字段、構造器)。
對JoinPoint進行攔截的定義。
攔截到JoinPoint後要執行的代碼,分爲前置、後置、環繞三種類型。
首先,爲了在Android使用AOP埋點須要引入AspectJ,在項目根目錄的build.gradle下加入:
classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'
複製代碼
而後,在app目錄下的build.gradle下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
複製代碼
JoinPoint通常定位在以下位置
使用PointCut對咱們指定的鏈接點進行攔截,經過Advice,就能夠攔截到JoinPoint後要執行的代碼。Advice一般有如下幾種類型:
首先,咱們舉一個小栗子:
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}
複製代碼
在 execution 中的是一個匹配規則,第一個 * 表明匹配任意的方法返回值,後面的語法代碼匹配全部Activity中on開頭的方法。
其中execution是處理Join Point的類型,在AspectJx中共有兩種類型,以下所示:
@Aspect
public class ApplicationAop {
@Around("call (* com.json.chao.application.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}
複製代碼
在上述代碼中,咱們須要注意 不一樣的Action類型其對應的方法入參是不一樣的,具體的差別以下所示:
ProceedingPoint不一樣於JoinPoint,其提供了proceed方法執行目標方法。
使用 Profile 的 CPU 模塊能夠幫咱們快速找到耗時的熱點方法,下面,咱們來詳細來分析一下這個模塊。
Trace types 有四種,以下所示。
會記錄每一個方法的時間、CPU信息。對運行時性能影響較大。
相比於Trace Java Methods會記錄每一個方法的時間、CPU信息,它會在應用的Java代碼執行期間頻繁捕獲應用的調用堆棧,對運行時性能的影響比較小,可以記錄更大的數據區域。
需部署到Android 8.0及以上設備,內部使用simpleperf跟蹤應用的native代碼,也能夠命令行使用simpleperf。
用於顯示應用程序在其生命週期中轉換不一樣狀態的活動,如用戶交互、屏幕旋轉事件等。
用於顯示應用程序 實時CPU使用率、其它進程實時CPU使用率、應用程序使用的線程總數。
列出應用程序進程中的每一個線程,並使用了不一樣的顏色在其時間軸上指示其活動。
Profile提供的檢查跟蹤數據窗口有四種,以下所示:
提供函數跟蹤數據的圖形表示形式。
右鍵點擊 Jump to source 跳轉至指定函數。
將具備相同調用方順序的徹底相同的方法收集起來。
看頂層的哪一個函數佔據的寬度最大(表現爲平頂),可能存在性能問題。
咱們在查看上面4個跟蹤數據的區域時,應該注意右側的兩個時間,以下所示:
主要作熱點分析,用來獲得如下兩種數據:
首先,咱們能夠定義一個Trace靜態工廠類,將Trace.begainSection(),Trace.endSection()封裝成i、o方法,而後再在想要分析的方法先後進行插樁便可。
而後,在命令行下執行systrace.py腳本,命令以下所示:
python /Users/quchao/Library/Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a "com.wanandroid.json.chao" -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html
複製代碼
具體參數含義以下:
在UIThread一欄能夠看到核心的系統方法時間區域和咱們本身使用代碼插樁捕獲的方法時間區域。
其中,Android Framework 裏面一些重要的模塊都插入了label信息,用戶App中也能夠添加自定義的Lable。
覆蓋高中低端機型不一樣的場景。
須要準確地統計啓動耗時。
是不是使用界面顯示且用戶真正能夠操做的時間做爲啓動結束時間。
閃屏、廣告和新手引導這些時間都應該從啓動時間裏扣除。
Broadcast、Server拉起,啓動過程進入後臺都須要排除統計。
一些體驗不好的用戶極可能被平均了。
如2s快開比,5s慢開比,能夠看到有多少比例的用戶體驗好,多少比例的用戶比較糟糕。
若是90%用戶的啓動時間都小於5s,那麼90%區間的啓動耗時就是5s。
借鑑Facebook的 profilo 工具原理,對啓動整個流程進行耗時監控,在後臺對不一樣的版本作自動化對比,監控新版本是否有新增耗時的函數。
Application、Activity建立以及回調等過程。
使用Activity的windowBackground主題屬性預先設置一個啓動圖片(layer-list實現),在啓動後,在Activity的onCreate()方法中的super.onCreate()前再setTheme(R.style.AppTheme)。
按需初始化,特別是針對於一些應用啓動時不須要初始化的庫,能夠等到用時才進行加載。
輪流獲取、均分CPU。
優先級高的獲取。
設置線程優先級。
它是一種更嚴格的羣組調度策略,主要分爲以下兩種類型:
由強大的調度器Scheduler集合提供。
不一樣類型的Scheduler:
特別適合Hook手段,找Hook點:構造函數或者特定方法,如Thread的構造函數。
這裏咱們直接使用維數的 epic 對Thread進行Hook。在attachBaseContext中調用DexposedBridge.hookAllConstructors方法便可,以下所示:
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override protected void afterHookedMethod(MethodHookParam param)throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
LogUtils.i("stack " + Log.getStackTraceString(new Throwable());
}
);
複製代碼
從log找到線程建立信息,根據堆棧信息跟相關業務方溝通解決方案。
直接依賴線程庫,但問題在於線程庫更新可能會致使基礎庫更新。
目前基礎線程池組件位於啓動器sdk之中,使用很是簡單,示例代碼以下所示:
// 若是當前執行的任務是CPU密集型任務,則從基礎線程池組件
// DispatcherExecutor中獲取到用於執行 CPU 密集型任務的線程池
DispatcherExecutor.getCPUExecutor().execute(YourRunable());
// 若是當前執行的任務是IO密集型任務,則從基礎線程池組件
// DispatcherExecutor中獲取到用於執行 IO 密集型任務的線程池
DispatcherExecutor.getIOExecutor().execute(YourRunable());
複製代碼
具體的實現源碼也比較簡單,而且我對每一處代碼都進行了詳細的解釋,就不一一具體分析了。代碼以下所示:
public class DispatcherExecutor {
/**
* CPU 密集型任務的線程池
*/
private static ThreadPoolExecutor sCPUThreadPoolExecutor;
/**
* IO 密集型任務的線程池
*/
private static ExecutorService sIOThreadPoolExecutor;
/**
* 當前設備能夠使用的 CPU 核數
*/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
/**
* 線程池核心線程數,其數量在2 ~ 5這個區域內
*/
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));
/**
* 線程池線程數的最大值:這裏指定爲了核心線程數的大小
*/
private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE;
/**
* 線程池中空閒線程等待工做的超時時間,當線程池中
* 線程數量大於corePoolSize(核心線程數量)或
* 設置了allowCoreThreadTimeOut(是否容許空閒核心線程超時)時,
* 線程會根據keepAliveTime的值進行活性檢查,一旦超時便銷燬線程。
* 不然,線程會永遠等待新的工做。
*/
private static final int KEEP_ALIVE_SECONDS = 5;
/**
* 建立一個基於鏈表節點的阻塞隊列
*/
private static final BlockingQueue<Runnable> S_POOL_WORK_QUEUE = new LinkedBlockingQueue<>();
/**
* 用於建立線程的線程工廠
*/
private static final DefaultThreadFactory S_THREAD_FACTORY = new DefaultThreadFactory();
/**
* 線程池執行耗時任務時發生異常所須要作的拒絕執行處理
* 注意:通常不會執行到這裏
*/
private static final RejectedExecutionHandler S_HANDLER = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
Executors.newCachedThreadPool().execute(r);
}
};
/**
* 獲取CPU線程池
*
* @return CPU線程池
*/
public static ThreadPoolExecutor getCPUExecutor() {
return sCPUThreadPoolExecutor;
}
/**
* 獲取IO線程池
*
* @return IO線程池
*/
public static ExecutorService getIOExecutor() {
return sIOThreadPoolExecutor;
}
/**
* 實現一個默認的線程工廠
*/
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "TaskDispatcherPool-" +
POOL_NUMBER.getAndIncrement() +
"-Thread-";
}
@Override
public Thread newThread(Runnable r) {
// 每個新建立的線程都會分配到線程組group當中
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon()) {
// 非守護線程
t.setDaemon(false);
}
// 設置線程優先級
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
static {
sCPUThreadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
S_POOL_WORK_QUEUE, S_THREAD_FACTORY, S_HANDLER);
// 設置是否容許空閒核心線程超時時,線程會根據keepAliveTime的值進行活性檢查,一旦超時便銷燬線程。不然,線程會永遠等待新的工做。
sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);
// IO密集型任務線程池直接採用CachedThreadPool來實現,
// 它最多能夠分配Integer.MAX_VALUE個非核心線程用來執行任務
sIOThreadPoolExecutor = Executors.newCachedThreadPool(S_THREAD_FACTORY);
}
}
複製代碼
項目發展階段忽視基礎設施建設,沒有采用統一的線程池,致使線程數量過多。
異步任務執行太耗時,致使主線程卡頓。
子線程分擔主線程任務,並行減小時間。
充分利用CPU多核,自動梳理任務順序。
啓動器的流程圖以下所示:
啓動器的主題流程爲上圖中的中間區域,即主線程與併發兩個區域塊。須要注意的是,在上圖中的 head task與tail task 並不包含在啓動器的主題流程中,它僅僅是用於處理啓動前/啓動後的一些通用任務,例如咱們能夠在head task中作一些獲取通用信息的操做,在tail task能夠作一些log輸出、數據上報等操做。
那麼,這裏咱們總結一下啓動的核心流程,以下所示:
下面,咱們就來使用異步啓動器來在Application的onCreate方法中進行異步優化,代碼以下所示:
// 一、啓動器初始化
TaskDispatcher.init(this);
// 二、建立啓動器實例,這裏每次獲取的都是新對象
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
// 三、給啓動器配置一系列的(異步/非異步)初始化任務並啓動啓動器
dispatcher
.addTask(new InitAMapTask())
.addTask(new InitStethoTask())
.addTask(new InitWeexTask())
.addTask(new InitBuglyTask())
.addTask(new InitFrescoTask())
.addTask(new InitJPushTask())
.addTask(new InitUmengTask())
.addTask(new GetDeviceIdTask())
.start();
// 四、須要等待微信SDK初始化完成,程序才能往下執行
dispatcher.await();
複製代碼
這裏的 TaskDispatcher 就是咱們的啓動器調用類。首先,在註釋1處,咱們須要先調用TaskDispatcher的init方法進行啓動器的初始化,其源碼以下所示:
public static void init(Context context) {
if (context != null) {
sContext = context;
sHasInit = true;
sIsMainProcess = Utils.isMainProcess(sContext);
}
}
複製代碼
能夠看到,僅僅是初始化了幾個基礎字段。接着,在註釋2處,咱們建立了啓動器實例,其源碼以下所示:
/**
* 注意:這裏咱們每次獲取的都是新對象
*/
public static TaskDispatcher createInstance() {
if (!sHasInit) {
throw new RuntimeException("must call TaskDispatcher.init first");
}
return new TaskDispatcher();
}
複製代碼
在createInstance方法的中咱們每次都會建立一個新的TaskDispatcher實例。而後,在註釋3處,咱們給啓動器配置了一系列的初始化任務並啓動啓動器,須要注意的是,這裏的Task既能夠是用於執行異步任務(子線程)的也能夠是用於執行非異步任務(主線程)。下面,咱們來分析下這兩種Task的用法,好比InitStethoTask這個異步任務的初始化,代碼以下所示:
/**
* 異步的Task
*/
public class InitStethoTask extends Task {
@Override
public void run() {
Stetho.initializeWithDefaults(mContext);
}
}
複製代碼
這裏的InitStethoTask直接繼承自Task,Task中的runOnMainThread方法返回爲false,說明 task 是用於處理異步任務的task,其中的run方法就是Runnable的run方法。下面,咱們再看看另外一個用於初始化非異步任務的例子,例如用於微信SDK初始化的InitWeexTask,代碼以下所示:
/**
* 主線程執行的task
*/
public class InitWeexTask extends MainTask {
@Override
public boolean needWait() {
return true;
}
@Override
public void run() {
InitConfig config = new InitConfig.Builder().build();
WXSDKEngine.initialize((Application) mContext, config);
}
}
複製代碼
能夠看到,它直接繼承了MainTask,MainTask的源碼以下所示:
public abstract class MainTask extends Task {
@Override
public boolean runOnMainThread() {
return true;
}
}
複製代碼
MainTask 直接繼承了Task,並僅僅是重寫了runOnMainThread方法返回了true,說明它就是用來初始化主線程中的非異步任務的。
此外,咱們注意到InitWeexTask中還重寫了一個needWait方法並返回了true,其目的是爲了在某個時刻以前必須等待InitWeexTask初始化完成程序才能繼續往下執行,這裏的某個時刻指的就是咱們在Application的onCreate方法中的註釋4處的代碼所執行的地方:dispatcher.await(),其實現源碼以下所示:
/**
* 須要等待的任務數
*/
private AtomicInteger mNeedWaitCount = new AtomicInteger();
/**
* 調用了 await 還沒結束且須要等待的任務列表
*/
private List<Task> mNeedWaitTasks = new ArrayList<>();
private CountDownLatch mCountDownLatch;
private static final int WAITTIME = 10000;
@UiThread
public void await() {
try {
// 一、僅僅在測試階段才輸出需等待的任務列表數與任務名稱
if (DispatcherLog.isDebug()) {
DispatcherLog.i("still has " + mNeedWaitCount.get());
for (Task task : mNeedWaitTasks) {
DispatcherLog.i("needWait: " + task.getClass().getSimpleName());
}
}
// 二、只要還有須要等待的任務沒有執行完成,就調用mCountDownLatch的await方法進行等待,這裏咱們設定超時時間爲10s
if (mNeedWaitCount.get() > 0) {
if (mCountDownLatch == null) {
throw new RuntimeException("You have to call start() before call await()");
}
mCountDownLatch.await(WAITTIME, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
}
}
複製代碼
首先,在註釋1處,咱們僅僅只會在測試階段纔會輸出需等待的任務列表數與任務名稱。而後,在註釋2處,只要須要等待的任務數mNeedWaitCount大於0,即只要還有須要等待的任務沒有執行完成,就調用mCountDownLatch的await方法進行等待,注意咱們這裏設定了超時時間爲10s。當一個task執行完成後,不管它是異步仍是非異步的,最終都會執行到mTaskDispatcher的markTaskDone(mTask)方法,咱們看看它的實現源碼,以下所示:
/**
* 已經結束的Task
*/
private volatile List<Class<? extends Task>> mFinishedTasks = new ArrayList<>(100);
public void markTaskDone(Task task) {
if (ifNeedWait(task)) {
mFinishedTasks.add(task.getClass());
mNeedWaitTasks.remove(task);
mCountDownLatch.countDown();
mNeedWaitCount.getAndDecrement();
}
}
複製代碼
能夠看到,這裏每執行完成一個task,就會將mCountDownLatch的鎖計數減1,與此同時,也會將咱們的mNeedWaitCount這個原子整數包裝類的數量減1。
此外,咱們在前面說到了啓動器將各個任務之間的依賴關係抽象成了一個有向無環圖,在上面一系列的初始化代碼中,InitJPushTask是須要依賴於GetDeviceIdTask的,那麼,咱們怎麼告訴啓動器它們二者之間的依賴關係呢?
這裏只須要在InitJPushTask中重寫dependsOn()方法,並返回包含GetDeviceIdTask的task列表便可,代碼以下所示:
/**
* InitJPushTask 須要在 getDeviceId 以後執行
*/
public class InitJPushTask extends Task {
@Override
public List<Class<? extends Task>> dependsOn() {
List<Class<? extends Task>> task = new ArrayList<>();
task.add(GetDeviceIdTask.class);
return task;
}
@Override
public void run() {
JPushInterface.init(mContext);
MyApplication app = (MyApplication) mContext;
JPushInterface.setAlias(mContext, 0, app.getDeviceId());
}
}
複製代碼
至此,咱們的異步啓動器就分析完畢了。下面咱們來看看如何高效地進行延遲初始化。
利用IdleHandler特性,在CPU空閒時執行,對延遲任務進行分批初始化。
延遲初始化啓動器的代碼很簡單,以下所示:
/**
* 延遲初始化分發器
*/
public class DelayInitDispatcher {
private Queue<Task> mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 分批執行的好處在於每個task佔用主線程的時間相對
// 來講很短暫,而且此時CPU是空閒的,這些能更有效地避免UI卡頓
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
複製代碼
在DelayInitDispatcher中,咱們提供了mDelayTasks隊列用於將每個task添加進來,使用者只需調用addTask方法便可。當CPU空閒時,mIdleHandler便會回調自身的queueIdle方法,這個時候咱們能夠將task一個一個地拿出來並執行。這種分批執行的好處在於每個task佔用主線程的時間相對來講很短暫,而且此時CPU是空閒的,這樣能更有效地避免UI卡頓,真正地提高用戶的體驗。
至於使用就很是簡單了,咱們能夠直接利用SplashActivity的廣告頁停留時間去進行延遲初始化,代碼以下所示:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
GlobalHandler.getInstance().getHandler().post((Runnable) () -> {
if (hasFocus) {
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new InitOtherTask())
.start();
}
});
}
複製代碼
須要注意的是,能異步的task咱們會優先使用異步啓動器在Application的onCreate方法中加載(或者是必須在Application的onCreate方法完成前必須執行完的非異task務),對於不能異步的task,咱們能夠利用延遲啓動器進行加載。若是任務能夠到用時再加載,能夠使用懶加載的方式。
咱們都知道,安裝或者升級後首次 MultiDex 花費的時間過於漫長,咱們須要進行Multidex的預加載優化。
5.0以上默認使用ART,在安裝時已將Class.dex轉換爲oat文件了,無需優化,因此應判斷只有在主進程及SDK 5.0如下才進行Multidex的預加載。
主要包括inline以及quick指令的優化。
使編譯器在函數調用處用函數體代碼代替函數調用指令。
函數調用的轉移操做有必定的時間和空間方面的開銷,特別是對於一些函數體不大且頻繁調用的函數,解決其效率問題更爲重要,引入inline函數就是爲了解決這一問題。
inline函數至少在三個方面提高了程序的時間性能:
爲了完全解決MutiDex加載時間慢的問題,抖音團隊深刻挖掘了 Dalvik 虛擬機的底層系統機制,對 DEX 相關的處理邏輯進行了從新設計與優化,並推出了 BoostMultiDex 方案,它可以減小 80% 以上的黑屏等待時間,挽救低版本 Android 用戶的升級安裝體驗。若有興趣的同窗能夠看看這篇文章:抖音BoostMultiDex優化實踐:Android低版本上APP首次啓動時間減小80%
能夠利用MultiDex預加載期間的這段CPU去預加載SharedPreferences。
需重寫getApplicationContext返回this,不然此時可能獲取不到context。
在Application中提早異步加載初始化耗時較長的類。
替換系統的ClassLoader,打印類加載的時間,按需選取須要異步加載的類。
在主頁空閒時,將其它頁面的數據加載好保存到內存或數據庫,等到打開該頁面時,判斷已經預加載過,就直接從內存或數據庫取數據並顯示。
子進程會共享CPU資源,致使主進程CPU緊張。此外,在多進程狀況下必定要能夠在onCreate中去區分進程作一些初始化工做。
App onCreate以前是ContentProvider初始化。
關於佈局與繪製優化能夠參考Android性能優化之繪製優化。
啓動時CG抑制,容許堆一直增加,直到手動或OOM中止GC抑制。(空間換時間)
須要白名單覆蓋全部設備,但維護成本高。
一個設備的CPU一般都是4核或者8核,可是應用在通常狀況下對CPU的利用率並不高,可能只有30%或者50%,若是咱們在啓動速度暴力拉伸CPU頻率,以此提升CPU的利用率,那麼,應用的啓動速度會提高很多。
在Android系統中,CPU相關的信息存儲在/sys/devices/system/cpu目錄的文件中,經過對該目錄下的特定文件進行寫值,實現對CPU頻率等狀態信息的更改。
暴力拉伸CPU頻率,致使耗電量增長。
對應的文件有:
這裏須要注意的是,須要考慮重度用戶的使用場景。
利用內存中的存儲空間來暫存從磁盤中讀出的一系列盤塊中的信息。所以,磁盤高速緩存在邏輯上屬於磁盤,物理上則是駐留在內存中的盤塊。
其內存中分爲兩種形式:
當數據寫入文件時,內核一般先將該數據複製到緩衝區高速緩存或頁面緩存中,若是該緩衝區還沒有寫滿,則不會將其排入輸入隊列,而是等待其寫滿或內核須要重用該緩衝區以便存放其餘磁盤塊數據時,再將該緩衝排入輸出隊列,最後等待其到達隊首時,才進行實際的IO操做—延遲寫。
延遲寫減小了磁盤讀寫次數,可是卻下降了文件內容的更新速度,可能會形成文件更新內容的丟失。爲了保證數據一致性,則需使用同步IO。
若是當前硬盤的平均尋道時間是3-15ms,7200RPM硬盤的平均旋轉延遲大約爲4ms,所以一次IO操做的耗時大約爲10ms。
若是使用內存映射文件的方式進行文件IO(mmap),將文件的page cache直接映射到進程的地址空間,這時須要使用msync系統調用確保修改的內容徹底同步到硬盤之上。
建立每一個log文件時先寫文件的最後一個page,將log文件擴展爲10MB大小,這樣即可以使用fdatasync,每寫10MB只有一次同步metadata的開銷。
標準IO,大多數文件系統默認的IO操做。
優勢
缺點
DMA方式能夠將數據直接從磁盤讀到頁緩存中,或者將數據從頁緩存中寫回到磁盤,而不能在應用程序地址空間和磁盤之間進行數據傳輸,這樣,數據在傳輸過程當中須要在應用程序地址空間(用戶空間)和緩存(內核空間)中進行屢次數據拷貝操做,這帶來的CPU以及內存開銷是很是大的。
磁盤IO主要的延時(15000RPM硬盤爲例)
機械轉動延時(平均2ms)+ 尋址延時(2~3ms)+ 塊傳輸延時(0.1ms左右)=> 平均5ms
網絡IO主要延時
服務器響應延時 + 帶寬限制 + 網絡延時 + 跳轉路由延時 + 本地接收延時(通常爲幾十毫秒到幾千毫秒,受環境影響極大)
很早以前,磁盤和內存之間的數據傳輸是須要CPU控制的,也就是讀取磁盤文件到內存中時,數據會通過CPU存儲轉發,這種方式稱爲PIO。
應用程序直接訪問磁盤數據,而不通過內核緩衝區。以減小從內核緩衝區到用戶數據緩存的數據複製。
當訪問數據的線程發出請求後,線程會接着去處理其它事情,而不是阻塞等待。
能夠爲訪問文件系統的系統調用提供一個統一的抽象接口。
Dex文件用到的類和APK裏面各類資源文件都比較小,讀取頻繁,且磁盤地址分佈範圍比較廣。咱們能夠利用Linux文件IO流程中的page cache機制將它們按照讀取順序從新排列在一塊兒,以減小真實的磁盤IO次數。
使用Facebook的 ReDex 的Interdex調整類在Dex中的排列順序。
一個能夠不修改APK就影響程序運行的Hook框架。
用自身實現的app_process替換掉系統/system/bin/app_process,加載一個額外的XposedBridge的jar包,用於將入口osZygoteInit.main()替換成XposedBridge.main()。以後,建立的Zygote進程和其子進程都是Hook過的了。
使用具體細節參見Xposed教程。
對象第一次建立的時候,JVM首先檢查對應的Class對象是否已經加載。若是沒有加載,JVM會根據類名查找.class文件,將其Class對象載入。同一個類第二次new的時候就不須要加載類對象,而是直接實例化,建立時間就縮短了。
ART比較複雜,Hook須要兼容幾個版本。並且在安裝時,大部分Dex已經優化好了,去掉ART平臺的verify只會對動態加載的Dex帶來一些好處。因此暫時不建議在ART平臺使用。
它們在設計上都存在大量的Hook和私有API調用,共同的缺點有以下兩類問題。
因爲廠商的兼容性、安裝失敗、ART加載時dex2oat失敗等緣由,仍是會有一些代碼和資源的異常。Android P推出的non-sdk-interface調用限制,之後適配只會愈來愈難,成本愈來愈高。
用到一些黑科技致使底層Runtime的優化享受不到。如Tinker加載補丁後,啓動速度會下降5%~10%。
Android官方使用熱補丁技術實現InstantRun。
構建 -> 部署 -> 安裝 -> 重啓app -> 重啓activity
儘量多的剔除沒必要要的步驟,而後提高必要步驟的速度。
一、HotSwap
增量構建 -> 改變部署
場景:
適用於多數簡單的改變(包括一些方法實現的修改,或者變量值修改)。
二、Warm Swap
增量構建 -> 改變部署 -> activity重啓
場景:
通常是修改了resources。
三、Cold Swap
增量構建 -> 改變部署 -> 應用重啓 -> activity重啓
場景:
涉及結構性變化,如修改了繼承規則或方法簽名。
Android Studio monitors 運行着Gradle任務來生成增量.dex文件(dex對應着開發中的修改類),AS會提取這些.dex文件發送到App Server,而後部署到App。由於原來版本的類都裝載在運行中的程序了,Gradle會解釋更新好這些.dex文件,發送到App Server的時候,交給自定義的類加載器來加載.dex文件。 App Server會不斷地監聽是否須要重寫類文件,若是須要,任務會被立馬執行,新的更改便能當即被響應。
須要注意的是,此時InstantRun是不能回退的,必須重啓應用響應修改。
由於資源文件是在Activity建立時加載,因此必須重啓Activity加載資源文件。
注意:AndroidManifest的值是在APK安裝的時候被讀取的,因此須要觸發一個完整的應用構建和部署。
應用部署的時候,會把工程拆分紅十個部分,每一個部分都擁有本身的.dex文件,而後全部的類會根據包名被分配給相應的.dex文件。當ColdSwap開啓時,修改過的類所對應的的.dex文件,會重組生成新的.dex文件,而後再部署到設備上。
注意:應用多進程會被降級爲ColdSwap。
manifest文件合併、打包,和res一塊兒被AAPT合併到APK中,同時項目代碼被編譯成字節碼,而後轉換成.dex文件,也被合併到APK中。
在回答這個問題以前,咱們須要先了解下內存對齊(DSA,Data Structure Alignment):
各類類型的數據按照必定的規則在內存空間上排列,這就是對齊。
複製代碼
內存對齊的優點在於可以以空間換時間,減小數據存取指令週期,提高程序運行時的速度。
zipalign優化的最根本目的是幫助操做系統更高效地根據請求索引資源,使用resource-handling code統一將DSA限定爲4byte。
利用build-tools文件夾下對應Android版本中的zipalign工具:
zipalign -v 4 source.apk androidres.apk
複製代碼
檢查當前APK是否已經執行過Align優化:
zipalign -c -v 4 androidres.apk
複製代碼
其中:
native hook -> dalvik_repleaceMethod -> 沒法支持新增或刪除filed的狀況 -> 需修復特定問題
它是一個基於Android Dex分包方案。它將多個dex文件放入到app的classloader中,可是android dex拆包方案中的類是沒有重複的,若是classes.dex和classes1.dex中有重複的類,當用到這個重複的類時,系統會選擇哪一個類進行加載呢?
一個ClassLoader能夠包含多個dex文件,每一個dex文件是一個Elements,多個dex文件排列成有序的dexElements,當找類的時候,會按順序遍歷dex文件,而後從當前遍歷的dex文件中找類,若是找到則返回,若是找不到從下一個dex文件繼續查找。
因此,若是在不一樣的dex中有相同的類存在,那麼會優先選擇排在前面的dex文件的類。
Qzone熱補丁方案就是把有問題的類打包到一個dex(patch.dex)中去,而後把這個dex插入到Elements的最前面。
一、當其它dex文件中的類引用了patch.dex中的類時,會出現校驗錯誤。拆分dex的不少類都不是在同一個dex內的,怎麼沒有問題?
由於這個校驗有個前提,當引用類被打上了CLASS_ISPREVERIFIED標誌,那麼就會進行dex的校驗。
二、CLASS_ISPREVERIFIED標誌是何時被打上去的?
有兩步驗證:
一、驗證clazz -> directMethods方法,其包含如下方法:
二、clazz -> virtualMethods
若是以上方法中直接引用到的類(第一層級關係,不會進行遞歸搜索)和clazz都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFED標誌。
爲了解決補丁方案中遇到的問題,因此必須從這些方法中入手,防止類被打上CLASS_ISPREVERIFIED標誌。空間的方案是往全部類的構造函數裏面插入一段代碼:
If (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
複製代碼
其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex中的類都會引用一個在不一樣dex中的AntilazyLoad類,這樣就防止類被打上了CLASS_ISPREVERIFILED標誌,只要沒被打上這個標誌的類均可以進行打補丁操做。
注意:
爲何要選擇構造函數?
由於他不增長方法數,一個類即便沒有顯示的構造函數,也有一個隱式的默認構造函數。
能夠使用ASM/javaassist庫在編譯期間將相應的字節碼插入Class文件中。
Art採用了新的方式,插樁對代碼的執行效率沒有影響。可是補丁中的類出現修改類變量或者方法,可能會致使出現內存地址錯亂的狀況。
緣由:
dex2oat時fast*已經將類能肯定的各個地址寫死。若是運行時補丁包的地址出現改變,原始類去調用時就會出現地址錯亂。
解決方法:
將其父類以及調用類的全部類都加入到補丁包中。
爲了提升性能。
因爲如今不少App都使用了MultiDex分包方案,這致使了不少類都沒有被打上這個標誌,因此此時禁用全部類打上CLASS_ISPREVERIFIED標誌對性能的影響不是很大。
在補丁包大小與性能損耗上有必定的侷限性。
插樁就是將一段代碼插入或者替換本來的代碼。 字節碼插樁就是在咱們的代碼編譯成字節碼(Class)後,在Android下生成dex以前修改Class文件,修改或者加強原有代碼邏輯的操做。
除了AspectJ、Javassist框架外,還有一個應用更爲普遍的ASM框架一樣也是字節碼操做框架,Instant Run包括Javassist就是藉助ASM來實現各自的功能。
能夠這樣理解Class字節碼與ASM之間的聯繫:
JSON對於GSON就相似於字節碼Class對於Javassist/ASM。
複製代碼
Android 1.5.0版本之後提供了Transform API,容許第三方Plugin在打包dex文件以前的編譯過程當中操做.class文件,咱們作的就是實現Transform進行.class文件遍歷拿到全部方法,修改完成後對文件進行替換。
大體的流程以下所示:
一、自動埋點追蹤,遍歷全部文件更換字節碼
AutoTransform -> transform -> inputs.each {TransformInput input -> input.jarInput.each { JarInput jarInput -> … } input.directoryInputs.each { DirectoryInput directoryInput -> … }}
複製代碼
二、Gradle插件實現
PluginEntry -> apply -> def android = project.extensions.getByType(AppExtension)
registerTransform(android) -> AutoTransform transform = new AutoTransform
android.registerTransform(transform)
複製代碼
三、使用ASM進行字節碼編寫
ASM框架核心類
一、visit -> 在ClassVisitor中根據判斷是不是實現View$OnClickListener接口的類,只有知足條件的類纔會遍歷其中的方法進行操做。
二、在MethodVisitor中對該方法進行修改
visitAnnotation -> onMethodEnter -> onMethodExit
複製代碼
三、先在java文件中編寫要插入的代碼,而後使用ASM插件查看對應的字節碼,根據其用ASM提供的Api一一對應地把代碼填進來便可。
關於編譯插樁的知識,筆者後面會有一系列的文章進行深刻講解,具體的文章目錄能夠在這裏查看。
DexDiff的粒度是Dex格式的每一項,BsDiff的粒度是文件,AndFix/Qzone的粒度爲class。
若不care性能損耗與補丁包大小,Qzone是最簡單且成功率最高的方案。
負責將補丁包交付給用戶,包括特定用戶和全量用戶。
一、pull通道
在登陸/24小時等時機,經過pull方式查詢後臺是否有對應的補丁包更新。
二、指定版本的push通道
在緊急狀況下,咱們能夠在一個小時內向全部用戶下發補丁包更新。
三、指定特定用戶的push通道
對特定用戶或用戶組作遠程調試。
快速上線,管理歷史記錄,以及監控補丁的運行狀況。
構建了App與系統(ROM)之間可靠的通訊框架,讓系統知道App的需求。
平均10%~30%。
一種優化資源調度的技術。
讓應用程序與系統資源實現實時"雙向對話"。當來自應用和遊戲程序的不一樣場景和用戶行爲被Hyper Boost識別後,手機會智能地匹配到合理的系統資源,讓手機SoC的CPU、GPU、ISP、DSP提供的運算資源更加合理地利用,從而讓用戶使用手機更加流暢。
在某一個版本以後呢,咱們會發現這個啓動速度變得特別慢,同時用戶給咱們的反饋也愈來愈多,因此,咱們開始考慮對應用的啓動速度來進行優化。而後,咱們就對啓動的代碼進行了代碼層面的梳理,咱們發現應用的啓動流程已經很是複雜,接着,咱們經過一系列的工具來確認是否在主線程中執行了太多的耗時操做。
咱們通過了細查代碼以後,發現應用主線程中的任務太多,咱們就想了一個方案去針對性地解決,也就是進行異步初始化。(引導=>第2題) 而後,咱們還發現了另一個問題,也能夠進行鍼對性的優化,就是在咱們的初始化代碼當中有些的優先級並非那麼高,它能夠不放在Application的onCreate中執行,而徹底能夠放在以後延遲執行的,由於咱們對這些代碼進行了延遲初始化,最後,咱們還結合了idealHandler作了一個更優的延遲初始化的方案,利用它能夠在主線程的空閒時間進行初始化,以減小啓動耗時致使的卡頓現象。作完這些以後,咱們的啓動速度就變得很快了。
最後,我簡單說下咱們是怎麼長期來保持啓動優化的效果的。首先,咱們作了咱們的啓動器,而且結合了咱們的CI,在線上加上了不少方面的監控。(引導=> 第4題)
咱們最初是採用的普通的一個異步的方案,即new Thread + 設置線程優先級爲後臺線程的方式在Application的onCreate方法中進行異步初始化,後來,咱們使用了線程池、IntentService的方式,可是,在咱們應用的演進過程中,發現代碼會變得不夠優雅,而且有些場景很是很差處理,好比說多個初始化任務直接的依賴關係,好比說某一個初始化任務須要在某一個特定的生命週期中初始化完成,這些都是使用線程池、IntentService沒法實現的。因此說,咱們就開始思考一個新的解決方案,它可以完美地解決咱們剛剛所遇到的這些問題。
這個方案就是咱們目前所使用的啓動器,在啓動器的概念中,咱們將每個初始化代碼抽象成了一個Task,而後,對它們進行了一個排序,根據它們之間的依賴關係排了一個有向無環圖,接着,使用一個異步隊列進行執行,而且這個異步隊列它和CPU的核心數是強烈相關的,它可以最大程度地保證咱們的主線程和別的線程都可以執行咱們的任務,也就是你們幾乎均可以同時完成。
首先,在CPU Profiler和Systrace中有兩個很重要的指標,即cpu time與wall time,咱們必須清楚cpu time與wall time之間的區別,wall time指的是代碼執行的時間,而cpu time指的是代碼消耗CPU的時間,鎖衝突會形成二者時間差距過大。咱們須要以cpu time來做爲咱們優化的一個方向。
其次,咱們不只只追求啓動速度上的一個提高,也須要注意延遲初始化的一個優化,對於延遲初始化,一般的作法是在界面顯示以後纔去進行加載,可是若是此時界面須要進行滑動等與用戶交互的一系列操做,就會有很嚴重的卡頓現象,所以咱們使用了idealHandler來實現cpu空閒時間來執行耗時任務,這極大地提高了用戶的體驗,避免了因啓動耗時任務而致使的頁面卡頓現象。
最後,對於啓動優化,還有一些黑科技,首先,就是咱們採用了類預先加載的方式,咱們在MultiDex.install方法以後起了一個線程,而後用Class.forName的方式來預先觸發類的加載,而後當咱們這個類真正被使用的時候,就不用再進行類加載的過程了。同時,咱們再看Systrace圖的時候,有一部分手機其實並無給咱們應用去跑滿cpu,好比說它有8核,可是卻只給了咱們4核等這些狀況,而後,有些應用對此作了一些黑科技,它會將cpu的核心數以及cpu的頻率在啓動的時候去進行一個暴力的提高。
這種問題其實咱們以前也遇到過,這的確很是難以解決。可是,咱們後面對此進行了反覆的思考與嘗試,終於找到了一個比較好的解決方式。
首先,咱們使用了啓動器去管理每個初始化任務,而且啓動器中每個任務的執行都是被其自動進行分配的,也就是說這些自動分配的task咱們會盡可能保證它會平均分配在咱們每個線程當中的,這和咱們普通的異步是不同的,它能夠很好地緩解咱們應用的啓動變慢。
其次,咱們還結合了CI,好比說,咱們如今限制了一些類,如Application,若是有人修改了它,咱們不會讓這部分代碼合併到主幹分支或者是修改以後會有一些內部的工具如郵件的形式發送到我,而後,我就會和他確認他加的這些代碼究竟是耗時多少,可否異步初始化,不能異步的話就考慮延遲初始化,若是初始化時間太長,則能夠考慮是否能進行懶加載,等用到的時候再去使用等等。
而後,咱們會將問題儘量地暴露在上線以前。同時,咱們真正已經到了線上的一個環境下時,咱們進行了監控的一個完善,咱們不只是監控了App的整個的啓動時間,同時呢,咱們也將每個生命週期都進行了一個監控。好比說Application的onCreate與onAttachBaseContext方法的耗時,以及這兩個生命週期之間間隔的時間,咱們都進行了一個監控,若是說下一次咱們發現了這個啓動速度變慢了,咱們就能夠去查找究竟是哪個環節變慢了,咱們會和之前的版本進行對比,對比完成以後呢,咱們就能夠來找這一段新加的代碼。
至此,探索Android啓動速度優化的旅途也應該告一段落了,若是你耐心讀到最後的話,會發現要想極致地提高App的性能,須要有必定的技術廣度,如咱們引入了始於後端的AOP編程來實現無侵入式的函數插樁,也須要有必定的深度,從前面的探索之旅來看,咱們前後涉及了Framework層、Native層、Dalvik虛擬機、甚至是Linux IO和文件系統相關的原理。所以,我想說,Android開發並不簡單,即便是App層面的性能優化這一知識體系,也是須要咱們不斷地加深自身知識的深度和廣度。
ps:在文章的黑科技部分涉及到了許多基礎架構研發領域的知識,這部分沒法理解的同窗不要灰心,先了解便可,
筆者以後的文章都會一一詳細講解。
複製代碼
二、支付寶客戶端架構解析:Android 客戶端啓動速度優化之「垃圾回收」
三、支付寶 App 構建優化解析:經過安裝包重排布優化 Android 端啓動性能
七、Dalvik Optimization and Verification With dexopt
八、微信在Github開源了Hardcoder,對Android開發者有什麼影響?
九、歷時三年研發,OPPO 的 Hyper Boost 引擎如何對系統、遊戲和應用實現加速?
十一、牆上時鐘時間 ,用戶cpu時間 ,系統cpu時間的理解
十二、《Android應用性能優化最佳實踐》
1三、必知必會 | Android 性能優化的方面方面都在這兒
1四、極客時間之Top團隊大牛帶你玩轉Android性能分析與優化
1五、啓動器源碼
1六、MultiDex優化源碼
歡迎關注個人微信:
bcce5360
微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~