Android效能最佳化之Splash頁應該這樣設計
目前SplashActivity的設計
目前市場上的應用在啟動時基本上都會先啟動一個SplashActivity,作為一個歡迎介面,為什麼這樣設計呢?
個人總結有三個優點:
1、可以給使用者更好的體驗
比如:可以由後台動態改變歡迎的圖片,或者顯歡迎xxx回來,新浪微博的就是這種互動。
2、可以縮減App的啟動時間
由上一篇博文中知道app啟動的耗時主要是在Application初始化中和MainActivity的介面繪製前,由於MainActivity的業務和布局複雜度肯定比只顯示一張圖片的介面高,所以,加入一個顯示一張圖片的Splash頁可以最佳化應用的啟動。
3、可以在應用啟動時做更多的事
一般來說SplashActivity一般會設計成停留2到4s不等,或者根據資料的載入程度來動態設定Splash介面的停留時間,既然停留那麼久,那麼當然可以在這個介面背後做一些事以備MainActivity的快速顯示,比如:資料的預先載入、sp的初始化、網路請求等。
當然你可能有些疑問,那這樣初始化放在Application中也可以啊?也用非同步作業資料也是一樣啊?
答案是不一樣!正如上篇所說的,Application初始化時並不會載入介面,而是在它建立完和初始化完成後,開始建立Activity時才開始繪製Theme中的background和繪製布局,所以用一個輕量的Splash頁給它設定一張背景歡迎圖,這樣就立馬能顯示介面了,而在這個介面中還可以做其它的初始化操作,這樣在視覺上即達到了app的快速啟動,又添加了體驗和做資料的初始化。
相反如果過多的放在Application中,則在點擊app表徵圖啟動時會感覺延遲,必須要把Application中的東西都做完才進入Activity的配置和繪製中。
目前大多數應用的Splash頁設計的不足之處
目前大多應用的Splash頁設計都是利用一個Activity,取名叫SplashActivity,然後在這個SplashActivity中加入一個背景圖,然後再new Handler().postDelayed()幾秒中,再startActivity跳入主介面,這樣設計看起來非常不錯,既可以在SplashActivity初始化、預先載入資料,還可以提高應用的啟動速度。
不過這確實提高了應用的啟動速度,畢竟我們比較快的看到了第一幀——SplashActivity,不過在SplashActivity之後,還需要調到MainActivity啊,雖然MainActivity中的一些資料可以在SplashActivity做預取,不過這中間需要有Intent的傳遞過程,而且MainActivity中布局還沒載入進來,所以還是需要再載入和繪製布局介面,然後才能填入資料,所以這樣看來,在跳轉到MainActivity中,還是需要做介面的繪製和資料的載入(包括Intent的資料傳遞)。
以往的SplashActivity的設計圖
這樣看來上面這個設計流程可以這樣表示:
效能優且體驗棒的Splash頁的設計
從上面這個設計圖來看,其中有些操作能不能去除呢?既能達到app啟動速度的提高,也能對資料的預先載入還能減去Splash和MainActivity之間不必要的資料傳遞和View的分開繪製。
答案是能的,既然SplashActivity和MainActivity分開進行操作還是不完美,那麼可以考慮把它們合為一起,即:一開始還是顯示MainActivity,SplashActivity變為SplashFragment,然後放一個FrameLayout作為根布局去顯示SplashFragment介面,這樣在SplashFragment顯示時候利用顯示的2~4s間的空隙時間做網路請求去載入資料,這樣待SplashFragment顯示完後再remove,這樣將看到的是有內容的MainActivity,就不必再去等待網路請求去返回資料了。
當然,這種方式是把load Splash View和ContentView合二為一了一起載入,這可能會影響應用的啟動時間,這時我們可以用ViewStub消極式載入MainActivity中某些View從而減去這個影響。
如下設計:
最佳化前後效果對比
這裡為了測試,我把Splash頁的delay時間都設為2.5s。
最佳化前:
最佳化後:
最佳化後其實是把SplashActivity用Fragment顯示,顯示完後再remove,這樣在顯示的時候,MainActivity中還可以直接載入網路資料,這樣在顯示完SplashFragment後則直接顯示首頁了,而省去了ProgressBar進度條的網路載入過程。
代碼:<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> private Handler mHandler = new Handler();//... final SplashFragment splashFragment = new SplashFragment(); final FragmentTransaction transaction = getFragmentManager().beginTransaction(); transaction.replace(R.id.frame, splashFragment); transaction.commit(); //... mHandler.postDelayed(new DelayRunnable(this, splashFragment, mProgressBar), 2500);//... static class DelayRunnable implements Runnable { private WeakReference contextRef; private WeakReference fragmentRef; private WeakReference progressBarRef; public DelayRunnable(Context context, SplashFragment splashFragment, ProgressBar progressBar) { contextRef = new WeakReference(context); fragmentRef = new WeakReference(splashFragment); progressBarRef = new WeakReference(progressBar); } @Override public void run() { ProgressBar progressBar = progressBarRef.get(); if (progressBar != null) progressBar.setVisibility(View.GONE); Activity context = (Activity) contextRef.get(); if (context != null) { SplashFragment splashFragment = fragmentRef.get(); if (splashFragment == null) return; final FragmentTransaction transaction = context.getFragmentManager().beginTransaction(); transaction.remove(splashFragment); transaction.commit(); } } } @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); }
其中FrameLayout作為MainActivity的根布局用作SplashFragment的全螢幕顯示。
為了更優則可以考慮ViewStub,在SplashFragment顯示時再進行載入額外的View。
關於耦合性,其實很低,Splash頁面有專門一個SplashFragment去配置,而MainActivity只是控制它的載入與remove。
關於應用DelayLoad消極式載入的幾種方式
1、View.postDelayed();
2、Handler.postDelayed();
3、getWindow().getDecorView().post(){Handler.postDelay()}
而前兩種方式雖然都有Delay效果,但並不是真正Delay了我們設定的時間,而第三種方式才是正確的Delay了我們想要的時間,原因如下:
關於Activity介面的啟動,首先是在onCreate()方法中會對contentView、DecorView和ActionBar等進行初始化,比如:contentView進行inflate,我們便可以在這個方法中通過findViewById來找到對應的控制項了,但是此時我們只是把那些對應的控制項建立了一個對象而已,它們並沒有add在介面上。而真正把contentView 添加到介面上的操作是在OnResume()方法執行時候,包括ViewRootImpl的初始化。而contentView的繪製會在onResume()方法執行完後的二次performTraversals()方法進行繪製。
所以要想達到DelayLoad懶載入效果,是在所有的View繪製完成後進行Delay效果,而上面的第一第二種方式都是在第一次performTraversals()後執行,該次只是為第二次調用performTraversals()方法做一些準備工作,這樣就達不到準確的Delay效果了,因為第二次的performTraversals()就是真正的進行View的測量布局和繪製了,這肯定是需要時間的,所以第一和第二種方式Delay的時間是需要再減去這個View繪製的時間的。
如果是想在介面剛繪製完成後做一些事情,或者有些事情必須在UI繪製完成顯示後做的話,那可以通過這個方法:
getWindow().getDecorView().post(new Runnable() { @Override public void run() { mHandler.post(new Runnable() { @Override public void run() { //do something } }); } });
如果想Delay的話,可以這樣:
getWindow().getDecorView().post(new Runnable() { @Override public void run() { mHandler.postDelayed(new Runnable() { @Override public void run() { //do something } },3000); } });