Android - 一種新奇的冷啓動速度優化思路(Fragment極度懶加載 + Layout子線程預加載)

1、背景

明天是週二,正好是咱們團隊每週一次的技術分享,我會把前段時間花了幾天在幹其餘活的同時,整的一套詭異的冷啓動速度優化方案分享一下。html

2、特地聲明

我這邊文章的內容不會涉及網上變地都是的常規的優化方案~ ,同時,平時工做的時候,工做內容雜且多,因此這個優化方案也不是特別成熟,僅供參考吧~android

3、最多見的優化方案

  • 數據懶加載,好比Fragment用戶不可見時不進行數據的獲取
  • 優化佈局層級,減小首次inflate layout的耗時
  • 將絕大部分sdk的初始化放線程池中運行
  • 能用ViewStub的就用ViewStub,按需加載layout
  • 必定要儘可能避免啓動過程當中,出現的主線程去unpack一些全局配置的數據
  • 不只僅是三方庫能夠放子線程進行,一些時效性要求沒那麼高的邏輯均可以放子線程


4、項目結構

在咱們的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

5、極致的懶加載

5.1 極致的懶加載

平時見到的懶加載:less

就是初始化fragment的時候,會連同咱們寫的網絡請求一塊兒執行,這樣很是消耗性能,最理想的方式是,只有用戶點開或滑動到當前fragment時,才進行請求網絡的操做。所以,咱們就產生了懶加載這樣一個說法。

可是。。。。異步

因爲咱們首屏4個子Tab都是繼承自一個基類BaseLoadListFragment,數據加載的邏輯很是的死,按照上述的改法,影響面太大。後續可能會徒增煩惱

5.2 懶加載方案

  1. 首屏加載時,只往ViewPager中塞入默認要展現的tab,剩餘的tab用空的佔位Fragment代替
  2. 當用戶滑動到其餘tab時,好比滑動到好友動態tab,就用FriendFragment把當前的EmptyPlaceholderFragment替換掉,而後adapter.notifyDataSetChanged
  3. 當四個Tab所有替換爲數據tab時,清除掉EmptyFragment的引用,釋放內存


說到這裏,又不得不提一個老生常談的一個坑,由於咱們的首頁是用的ViewPager + FragmentPagerAdapter來進行實現的。所以就出現了一個坑:

ViewPager + FragmentPagerAdapter組合使用,調用notifyDataSetChanged()方法無效,沒法刷新Fragment列表

下面我會對這個問題進行一下詳細的介紹

5.3 FragmentPagerAdapter與FragmentStatePagerAdapter

當咱們要使用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, consider FragmentStatePagerAdapter.

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 to FragmentPagerAdapter at the cost of potentially more overhead when switching between pages

總結:

  • 使用FragmentStatePagerAdapter時,若是tab對於用戶不可見了,Fragment就會被銷燬,FragmentPagerAdapter則不會,使用FragmentPagerAdapter時,全部的tab上的Fragment都會hold在內存裏
  • 當tab很是多時,推薦使用FragmentStatePagerAdapter
  • 當tab很少,且固定時,推薦用FragmentPagerAdapter

咱們項目中就是使用的ViewPager+FragmentPagerAdapter。

5.4 FragmentPagerAdapter的刷新問題

正常狀況,咱們使用adapter時,想要刷新數據只須要:

  1. 更新dataSet
  2. 調用notifyDataSetChanged()

可是,這個在這個Adapter中是不適用的。由於(這一步沒耐心的能夠直接看後面的總結):

  1. 默認的PagerAdapter的destoryItem只會把Fragment detach掉,而不會remove
  2. 當再次調用instantiateItem的時候,以前detach掉的Fragment,又會從mFragmentManager中取出,又能夠attach了


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。


5.5 FragmentPagerAdapter刷新的正確姿式

重寫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:什麼都沒作
1:開始滑動
2:滑動結束

一次引發頁面切換的滑動,state的順序分別是: 1  ->  2  ->  0

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;
            }
        }
    }
複製代碼


6、神奇的的預加載(預加載View,而不是data)

Android在啓動過程當中可能涉及到的一些View的預加載方案:

  1. WebView提早建立好,由於webview建立的耗時較長,若是首屏有h5的頁面,能夠提早建立好。
  2. Application的onCreate時,就能夠開始在子線程中進行後面要用到的Layout的inflate工做了,最早想到的應該是官方提供的AsyncLayoutInflater
  3. 填充View的數據的預加載,今天的內容不涉及這一項

6.1 須要預加載什麼

直接看圖,這個是首頁四個子Tab Fragment的基類的layout,由於某些東西設計的不合理,致使層級是很是的深,直接致使了首頁上的三個tab加上FeedMainFragment自身,光將這個View inflate出來的時間就很是長。所以咱們考慮在子線程中提早inflate layout



6.2 修改AsyncLayoutInflater

官方提供了一個類,能夠來進行異步的inflate,可是有兩個缺點:

  1. 每次都要現場new一個出來
  2. 異步加載的view只能經過callback回調才能得到(死穴)


所以決定本身封裝一個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;
    }
}
複製代碼

6.3 裝飾器模式

能夠看到Android上Context的設計採用了裝飾器模式,裝飾器模式極大程度的提升了靈活性。這個例子對我最大的感覺就是,當官方沒有提供MutableContextWrapper這個類時,其實咱們本身也徹底能夠經過一樣的方式去進行實現。思惟必定要靈活~


7、總結

常見的啓動速度優化的方案有:

  • 數據懶加載,好比Fragment用戶不可見時不進行數據的獲取
  • 優化佈局層級,減小首次inflate layout的耗時
  • 將絕大部分sdk的初始化放線程池中運行
  • 能用ViewStub的就用ViewStub,按需加載layout
  • 必定要儘可能避免啓動過程當中,出現的主線程去unpack一些全局配置的數據
  • 不只僅是三方庫能夠放子線程進行,一些時效性要求沒那麼高的邏輯均可以放子線程

這些均可以在網上找到大量的文章以及各個大佬的實現方案。

首先,優化的大方向確定先定好:

  1. 懶加載
  2. 預加載

懶加載:

  • 首屏加載時,只往ViewPager中塞入默認要展現的tab,剩餘的tab用空的佔位Fragment代替
  • 當用戶滑動到其餘tab時,好比滑動到好友動態tab,就用FriendFragment把當前的EmptyPlaceholderFragment替換掉,而後adapter.notifyDataSetChanged
  • 當四個Tab所有替換爲數據tab時,清除掉EmptyFragment的引用,釋放內存

預加載:

  • Application onCreate方法中,針對後續全部的Fragment,在子線程中將Layout先給inflate出來
  • 針對inflate完成的View加入一套緩存的存取機制,以及等待機制
    • 若是正在inflate,則進行阻塞等待
    • 若是已經inflate完成了,取出view,並釋放緩存對於View的引用
    • 若是尚未開始Inflate,則在UI線程直接進行inflate

這些方案不必定很是好使,因此僅供參考~~~

從ContextWrapper、MutableContextWrapper類的設計中學到了  ↓

寫代碼的時候,首先要進行設計,選用最合適的設計模式,這樣後續賺到的遠遠大於寫一個文檔、想一個設計所耗費的時間和腦力成本






個人簡書 鄒啊濤濤濤的簡書

個人CSDN 鄒啊濤濤濤的CSDN

個人掘金 鄒啊濤濤濤的掘金

相關文章
相關標籤/搜索