明天是週二,正好是咱們團隊每週一次的技術分享,我會把前段時間花了幾天在幹其餘活的同時,整的一套詭異的冷啓動速度優化方案分享一下。html
我這邊文章的內容不會涉及網上變地都是的常規的優化方案~ ,同時,平時工做的時候,工做內容雜且多,因此這個優化方案也不是特別成熟,僅供參考吧~android
在咱們的Android項目中,應用過了閃屏以後會進入到主屏 - MainActivity,這個地方我吐槽不少次了,廣告閃屏做爲launcher真的不是特別靠譜,最好的方式應該是從MainActivity裏面來啓動AdActivity,甚至是不用Activity,採用一個全屏的AdView均可以。web
先簡單介紹一下咱們項目中MainActivity涉及到的結構:設計模式
簡單的畫了個圖,簡直是。。畫圖界的恥辱。。。緩存
大概看看意思就能夠了,我在組內分享就是用的這個草圖,急着下班,就不從新畫了。。bash
當App冷啓動的時候,肉眼可見的要初始化的東西太多了,自己Fragment就是一個相對重的東西。比Activity要輕量不少,可是比View又要重
網絡
咱們首頁大概是 4-5個tab,每一個tab都是一個Fragment,且第一個tab內嵌了4個Fragment,我這一次的優化主要將目標瞄準了首頁的 tab1 以及tab1內嵌的四個tabapp
平時見到的懶加載:less
就是初始化fragment的時候,會連同咱們寫的網絡請求一塊兒執行,這樣很是消耗性能,最理想的方式是,只有用戶點開或滑動到當前fragment時,才進行請求網絡的操做。所以,咱們就產生了懶加載這樣一個說法。
可是。。。。異步
因爲咱們首屏4個子Tab都是繼承自一個基類BaseLoadListFragment,數據加載的邏輯很是的死,按照上述的改法,影響面太大。後續可能會徒增煩惱
說到這裏,又不得不提一個老生常談的一個坑,由於咱們的首頁是用的ViewPager + FragmentPagerAdapter來進行實現的。所以就出現了一個坑:
ViewPager + FragmentPagerAdapter組合使用,調用notifyDataSetChanged()方法無效,沒法刷新Fragment列表
下面我會對這個問題進行一下詳細的介紹
當咱們要使用ViewPager來加載Fragment時,官方爲咱們提供了這兩種Adapter,都是繼承自PagerAdapter。
區別,上官方描述:
FragmentPagerAdapter
This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, considerFragmentStatePagerAdapter
.
FragmentStatePagerAdapter
This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared toFragmentPagerAdapter
at the cost of potentially more overhead when switching between pages
總結:
咱們項目中就是使用的ViewPager+FragmentPagerAdapter。
正常狀況,咱們使用adapter時,想要刷新數據只須要:
可是,這個在這個Adapter中是不適用的。由於(這一步沒耐心的能夠直接看後面的總結):
3,ViewPager的dataSetChanged代碼以下:
4,且adapter的默認實現
簡單總結一下:
1,ViewPager的dataSetChanged()中會去用adapter.getItemPosition來判斷是否要移除當前Item(position = POSITION_NONE時remove)
2,PagerAdapter的getItemPosition默認實現爲POSITION_UNCHANGED
上述兩點致使ViewPager構建完成Adapter以後,不會有機會調用到Adapter的instantiateItem了。
再者,即便重寫了getItemPosition方法,每次返回POSITION_NONE,仍是不會替換掉Fragment,這是由於instantiateItem方法中,會根據getItemId()去從FragmetnManager中找到已經建立好的Fragment返回回去,而getItemId()的默認實現是return position。
重寫getItemId()和getItemPosition()
class TabsAdapter extends FragmentPagerAdapter {
private ArrayList<Fragment> mFragmentList;
private ArrayList<String> mPageTitleList;
private int mCount;
TabsAdapter(FragmentManager fm, ArrayList<Fragment> fragmentList, ArrayList<String> pageTitleList) {
super(fm);
mFragmentList = fragmentList;
mCount = fragmentList.size();
mPageTitleList = pageTitleList;
}
@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
@Override
public CharSequence getPageTitle(int position) {
return mPageTitleList.get(position);
}
@Override
public int getCount() {
return mCount;
}
@Override
public long getItemId(int position) {
//這個地方的重寫很是關鍵,super中是返回position,
//若是不重寫,仍是會繼續找到FragmentManager中緩存的Fragment
return mFragmentList.get(position).hashCode();
}
@Override
public int getItemPosition(@NonNull Object object) {
//不在數據集合裏面的話,return POSITION_NONE,進行item的重建
int index = mFragmentList.indexOf(object);
if (index == -1) {
return POSITION_NONE;
} else {
return mFragmentList.indexOf(object);
}
}
void refreshFragments(ArrayList<Fragment> fragmentList) {
mFragmentList = fragmentList;
notifyDataSetChanged();
}
}複製代碼
其餘的相關代碼:
(1)實現ViewPager.OnPageChangeListener,來監控ViewPager的滑動狀態,才能夠在滑動到下一個tab的時候進行Fragment替換的操做,其中mDefaultTab是咱們經過接口返回的當前啓動展現的tab序號
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
mCurrentSelectedTab = position;
}
@Override
public void onPageScrollStateChanged(int state) {
if (!hasReplacedAllEmptyFragments && mCurrentSelectedTab != mDefaultTab && state == 0) {
//當知足: 1. 沒有所有替換完 2. 當前tab不是初始化的默認tab(默認tab不會用空的Fragment去替換) 3. 滑動結束了,即state = 0
replaceEmptyFragmentsIfNeed(mCurrentSelectedTab);
}
}複製代碼
備註:
onPageScrollStateChanged接滑動的狀態值。一共有三個取值:
0:什麼都沒作
一次引發頁面切換的滑動,state的順序分別是: 1 -> 2 -> 0
1:開始滑動
2:滑動結束
(2)進行Fragment的替換,這裏由於咱們的tab數量是可能根據全局config信息而改變的,因此這個地方寫的稍微糾結了一些。
/**
* 若是所有替換完了,直接return
* 替換過程:
* 1. 找到當前空的tab在mEmptyFragmentList 中的實際下標
*
* @param tabId 要替換的tab的tabId - (當前空的Fragment在adapter數據列表mFragmentList的下標)
*/
private void replaceEmptyFragmentsIfNeed(int tabId) {
if (hasReplacedAllEmptyFragments) {
return;
}
int tabRealIndex = mEmptyFragmentList.indexOf(mFragmentList.get(tabId)); //找到當前的空Fragment在 mEmptyFragmentList 是第幾個
if (tabRealIndex > -1) {
if (Collections.replaceAll(mFragmentList, mEmptyFragmentList.get(tabRealIndex), mDataFragmentList.get(tabRealIndex))) {
mTabsAdapter.refreshFragments(mFragmentList); //將mFragmentList中的相應empty fragment替換完成以後刷新數據
boolean hasAllReplaced = true;
for (Fragment fragment : mFragmentList) {
if (fragment instanceof EmptyPlaceHolderFragment) {
hasAllReplaced = false;
break;
}
}
if (hasAllReplaced) {
mEmptyFragmentList.clear(); //所有替換完成的話,釋放引用
}
hasReplacedAllEmptyFragments = hasAllReplaced;
}
}
}
複製代碼
Android在啓動過程當中可能涉及到的一些View的預加載方案:
直接看圖,這個是首頁四個子Tab Fragment的基類的layout,由於某些東西設計的不合理,致使層級是很是的深,直接致使了首頁上的三個tab加上FeedMainFragment自身,光將這個View inflate出來的時間就很是長。所以咱們考慮在子線程中提早inflate layout
官方提供了一個類,能夠來進行異步的inflate,可是有兩個缺點:
所以決定本身封裝一個AsyncInflateManager,內部使用線程池,且對於inflate完成的View有一套緩存機制。而其中最核心的LayoutInflater則直接copy出來就好。
先看AsyncInflateManager的實現,這裏我直接將代碼copy進來,而不是截圖了,這樣大家若是想用其中部分東西,能夠直接copy:
/**
* @author zoutao
* <p>
* 用來提供子線程inflate view的功能,避免某個view層級太深太複雜,主線程inflate會耗時很長,
* 實就是對 AsyncLayoutInflater進行了抽取和封裝
*/
public class AsyncInflateManager {
private static AsyncInflateManager sInstance;
private ConcurrentHashMap<String, AsyncInflateItem> mInflateMap; //保存inflateKey以及InflateItem,裏面包含全部要進行inflate的任務
private ConcurrentHashMap<String, CountDownLatch> mInflateLatchMap;
private ExecutorService mThreadPool; //用來進行inflate工做的線程池
private AsyncInflateManager() {
mThreadPool = new ThreadPoolExecutor(4, 4, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>());
mInflateMap = new ConcurrentHashMap<>();
mInflateLatchMap = new ConcurrentHashMap<>();
}
public static AsyncInflateManager getInstance() {
單例
}
/**
* 用來得到異步inflate出來的view
*
* @param context
* @param layoutResId 須要拿的layoutId
* @param parent container
* @param inflateKey 每個View會對應一個inflateKey,由於可能許多地方用的同一個 layout,可是須要inflate多個,用InflateKey進行區分
* @param inflater 外部傳進來的inflater,外面若是有inflater,傳進來,用來進行可能的SyncInflate,
* @return 最後inflate出來的view
*/
@UiThread
@NonNull
public View getInflatedView(Context context, int layoutResId, @Nullable ViewGroup parent, String inflateKey, @NonNull LayoutInflater inflater) {
if (!TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) {
AsyncInflateItem item = mInflateMap.get(inflateKey);
CountDownLatch latch = mInflateLatchMap.get(inflateKey);
if (item != null) {
View resultView = item.inflatedView;
if (resultView != null) {
//拿到了view直接返回
removeInflateKey(inflateKey);
replaceContextForView(resultView, context);
return resultView;
}
if (item.isInflating() && latch != null) {
//沒拿到view,可是在inflate中,等待返回
try {
latch.wait();
} catch (InterruptedException e) {
Log.e(TAG, e.getMessage(), e);
}
removeInflateKey(inflateKey);
if (resultView != null) {
replaceContextForView(resultView, context);
return resultView;
}
}
//若是還沒開始inflate,則設置爲false,UI線程進行inflate
item.setCancelled(true);
}
}
//拿異步inflate的View失敗,UI線程inflate
return inflater.inflate(layoutResId, parent, false);
}
/**
* inflater初始化時是傳進來的application,inflate出來的view的context無法用來startActivity,
* 所以用MutableContextWrapper進行包裝,後續進行替換
*/
private void replaceContextForView(View inflatedView, Context context) {
if (inflatedView == null || context == null) {
return;
}
Context cxt = inflatedView.getContext();
if (cxt instanceof MutableContextWrapper) {
((MutableContextWrapper) cxt).setBaseContext(context);
}
}
@UiThread
private void asyncInflate(Context context, AsyncInflateItem item) {
if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {
return;
}
onAsyncInflateReady(item);
inflateWithThreadPool(context, item);
}
private void onAsyncInflateReady(AsyncInflateItem item) {
...
}
private void onAsyncInflateStart(AsyncInflateItem item) {
...
}
private void onAsyncInflateEnd(AsyncInflateItem item, boolean success) {
item.setInflating(false);
CountDownLatch latch = mInflateLatchMap.get(item.inflateKey);
if (latch != null) {
//釋放鎖
latch.countDown();
}
...
}
private void removeInflateKey(String inflateKey) {
...
}
private void inflateWithThreadPool(Context context, AsyncInflateItem item) {
mThreadPool.execute(new Runnable() {
@Override
public void run() {
if (!item.isInflating() && !item.isCancelled()) {
try {
onAsyncInflateStart(item);
item.inflatedView = new BasicInflater(context).inflate(item.layoutResId, item.parent, false);
onAsyncInflateEnd(item, true);
} catch (RuntimeException e) {
Log.e(TAG, "Failed to inflate resource in the background! Retrying on the UI thread", e);
onAsyncInflateEnd(item, false);
}
}
}
});
}
/**
* copy from AsyncLayoutInflater - actual inflater
*/
private static class BasicInflater extends LayoutInflater {
private static final String[] sClassPrefixList = new String[]{"android.widget.", "android.webkit.", "android.app."};
BasicInflater(Context context) {
super(context);
}
public LayoutInflater cloneInContext(Context newContext) {
return new BasicInflater(newContext);
}
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = this.createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException ignored) {
}
}
return super.onCreateView(name, attrs);
}
}
}
複製代碼
這裏我用一個AsyncInflateItem來管理一次要inflate的一個單位,
/**
* @author zoutao
*/
public class AsyncInflateItem {
String inflateKey;
int layoutResId;
ViewGroup parent;
OnInflateFinishedCallback callback;
View inflatedView;
private boolean cancelled;
private boolean inflating;
//還有一些set get方法
}複製代碼
以及最後inflate的回調callback:
public interface OnInflateFinishedCallback {
void onInflateFinished(AsyncInflateItem result);
}複製代碼
通過這樣的封裝,外面能夠直接在Application的onCreate中,開始異步的inflate view的任務。調用以下:
AsyncInflateUtil.startTask();複製代碼
/**
* @author zoutao
*/
public class AsyncInflateUtil {
public static void startTask() {
Context context = new MutableContextWrapper(CommonContext.getApplication());
AsyncInflateManager.getInstance().asyncInflateViews(context,
new AsyncInflateItem(InflateKey.TAB_1_CONTAINER_FRAGMENT, R.layout.fragment_main),
new AsyncInflateItem(InflateKey.SUB_TAB_1_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_2_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_3_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_4_FRAGMENT, R.layout.fragment_load_list));
}
public class InflateKey {
public static final String TAB_1_CONTAINER_FRAGMENT = "tab1";
public static final String SUB_TAB_1_FRAGMENT = "sub1";
public static final String SUB_TAB_2_FRAGMENT = "sub2";
public static final String SUB_TAB_3_FRAGMENT = "sub3";
public static final String SUB_TAB_4_FRAGMENT = "sub4";
}
}
複製代碼
注意:這裏會有一個坑。就是在Application的onCreate中,能拿到的Context只有Application,這樣inflate的View,View持有的Context就是Application,這會致使一個問題。
若是用View.getContext()這個context去進行Activity的跳轉就會。。拋異常
Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
而若是想要傳入Activity來建立LayoutInflater,時機又太晚。衆所周知,Context是一個抽象類,實現它的包裝類就是ContextWrapper,而Activity、Appcation等都是ContextWrapper的子類,然而,ContextWrapper還有一個神奇的子類,
package android.content;
/**
* Special version of {@link ContextWrapper} that allows the base context to
* be modified after it is initially set.
*/
public class MutableContextWrapper extends ContextWrapper {
public MutableContextWrapper(Context base) {
super(base);
}
/**
* Change the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Unlike ContextWrapper, the base context
* can be changed even after one is already set.
*
* @param base The new base context for this wrapper.
*/
public void setBaseContext(Context base) {
mBase = base;
}
}
複製代碼
能夠看到Android上Context的設計採用了裝飾器模式,裝飾器模式極大程度的提升了靈活性。這個例子對我最大的感覺就是,當官方沒有提供MutableContextWrapper這個類時,其實咱們本身也徹底能夠經過一樣的方式去進行實現。思惟必定要靈活~
常見的啓動速度優化的方案有:
這些均可以在網上找到大量的文章以及各個大佬的實現方案。
首先,優化的大方向確定先定好:
懶加載:
預加載:
這些方案不必定很是好使,因此僅供參考~~~
從ContextWrapper、MutableContextWrapper類的設計中學到了 ↓
個人簡書 鄒啊濤濤濤的簡書
個人CSDN 鄒啊濤濤濤的CSDN
個人掘金 鄒啊濤濤濤的掘金