面試總結(jié)(5):Fragment的懶加載

前言#

在我們的項目里經(jīng)常會用到ViewPager+Fragment實現(xiàn)選項卡滑動切換的效果,ViewPager會預(yù)加載下一個Framgment的內(nèi)容,這樣的機制有優(yōu)點也有缺點:

預(yù)加載讓用戶可以更快的看到接下來的內(nèi)容,瀏覽起來連貫性更好,但是app在展示內(nèi)容的同時還增加了額外的任務(wù),這樣可能影響界面的流暢度,并且可能造成流量的浪費。

目前大部分的app都使用Fragment懶加載機制,例如嗶哩嗶哩,360手機助手等等。

正文#

實現(xiàn)Fragment懶加載也可以有很多種辦法,可能有些朋友會第一時間想到通過監(jiān)聽滾動的位置,通過判斷Fragment的加載狀態(tài)實現(xiàn)懶加載,這種方法的缺點就是把實現(xiàn)暴露在Framgment之外,從封裝的角度來說這不是一個好的方案。

最好的方案在網(wǎng)上已經(jīng)隨處可見了,那就是重寫Fragment的setUserVisibleHint()方法,實現(xiàn)Fragment內(nèi)部的懶加載機制。

首先我們看看這個方法的注釋:

/**
* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* <p>An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.</p>
*
* <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/

英文有點多,簡單的翻譯總結(jié)一下就是,F(xiàn)ramgent沒有直接顯示給用戶(例如滾動出了屏幕)會返回false,值得注意的是這個方法可能在Fragment的生命周期以外調(diào)用。

官方已經(jīng)提示這個方法可能在生命周期之外調(diào)用,先看看這個方法到底是在什么時候會被調(diào)用:

這里寫圖片描述

我在Fragment中打印了Fragment生命周期的幾個比較重要的方法,從log上看setUserVisibleHint()的調(diào)用早于onCreateView,所以如果在setUserVisibleHint()要實現(xiàn)懶加載的話,就必須要確保View以及其他變量都已經(jīng)初始化結(jié)束,避免空指針。

然后滑動ViewPager,看一下運行情況:

這里寫圖片描述

滑動的時候,兩個Fragment的setUserVisibleHint()方法被調(diào)用,顯示狀態(tài)也發(fā)生了變化。

ok,那就可以直接寫代碼了,先定義我們的MyFragment:

/**
 * Created by li.zhipeng on 2017/5/7.
 * <p>
 * Fragment
 */

public class MyFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener{

    private SwipeRefreshLayout swipeRefreshLayout;
    private ListView listView;
    private Handler handler = new Handler();

    /**
     * 是否已經(jīng)初始化完成
     * */
    private boolean isPrepare;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        Log.e("lzp", "onCreateView" + hashCode());
        return inflater.inflate(R.layout.fragment_my, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Log.e("lzp", "onViewCreate" + hashCode());
        swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipeRefreshLayout);
        listView = (ListView) view.findViewById(R.id.listView);
        swipeRefreshLayout.setOnRefreshListener(this);
        // 初始化完成
        isPrepare = true;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.e("lzp", "onActivityCreated" + hashCode());
        // 創(chuàng)建時要判斷是否已經(jīng)顯示給用戶,加載數(shù)據(jù)
        onVisibleToUser();
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.e("lzp", "setUserVisibleHint" + hashCode() + ":" + isVisibleToUser);
        // 顯示狀態(tài)發(fā)生變化
        onVisibleToUser();
    }

    /**
     * 顯示給用戶的操作
     * */
    private void onVisibleToUser(){
        if (isPrepare && getUserVisibleHint()){
            onRefresh();
        }
    }

    /**
     * 加載數(shù)據(jù)
     * */
    private void loadData() {
        listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, new String[]{
                "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111",
                "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111"
        }));
    }

    @Override
    public void onRefresh() {
       // 只加載一次數(shù)據(jù),避免界面切換的時候,加載數(shù)據(jù)多次
        if (listView.getAdapter() == null){
            swipeRefreshLayout.setRefreshing(true);
            new Thread(){
                @Override
                public void run() {
                    // 延遲1秒
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            loadData();
                            swipeRefreshLayout.setRefreshing(false);
                        }
                    });
                }
            }.start();
        }
    }
}

效果圖我就不貼出來了,最后會有源碼鏈接,大家可以下載運行看看效果。

這樣就結(jié)束了嗎?那肯定不行,雖然了解了用法,但是不封裝一下,以后就要寫很多次,簡直不要太low。

封裝之后的BaseFragment的代碼:

/**
 * Created by li.zhipeng on 2017/5/8.
 * <p>
 * Fragment 的基類
 */

public abstract class BaseFragment extends Fragment {

    /**
     * 布局id
     */
    private View contentView;

    /**
     * 是否啟用懶加載,此屬性僅對BaseLazyLoadFragment有效
     * */
    private boolean isLazyLoad;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        contentView = inflater.inflate(setLayoutId(), container, false);
        return contentView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 初始化
        init();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // 如果不是懶加載模式,創(chuàng)建就加載數(shù)據(jù)
        if (!isLazyLoad){
            loadData();
        }
    }

    /**
     * 設(shè)置加載的布局Id
     */
    protected abstract int setLayoutId();

    /**
     * 初始化操作
     */
    protected abstract void init();

    /**
     * findViewById
     */
    protected View findViewById(int id) {
        return contentView.findViewById(id);
    }

    /**
     * 懶加載數(shù)據(jù)
     */
    protected abstract void loadData();


    /**
     * 是否啟用懶加載,此方法僅對BaseLazyLoadFragment有效
     * */
    public void setLazyLoad(boolean lazyLoad) {
        isLazyLoad = lazyLoad;
    }
}

接下來是BaseLazyLoadFragment:

/**
 * Created by li.zhipeng on 2017/5/8.
 *
 *  懶加載的Fragment
 */

public abstract class BaseLazyLoadFragment extends BaseFragment {

    /**
     * 是否已經(jīng)初始化結(jié)束
     */
    private boolean isPrepare;

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        setLazyLoad(true);
        isPrepare = true;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // 創(chuàng)建時要判斷是否已經(jīng)顯示給用戶,加載數(shù)據(jù)
        onVisibleToUser();
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        // 顯示狀態(tài)發(fā)生變化
        onVisibleToUser();
    }

    /**
     * 判斷是否需要加載數(shù)據(jù)
     */
    private void onVisibleToUser() {
        // 如果已經(jīng)初始化完成,并且顯示給用戶
        if (isPrepare && getUserVisibleHint()) {
            loadData();
        }
    }
}

最后看看MyFragment的代碼:

/**
 * Created by li.zhipeng on 2017/5/7.
 * <p>
 * Fragment
 */

public class MyFragment extends BaseLazyLoadFragment implements SwipeRefreshLayout.OnRefreshListener {

    private SwipeRefreshLayout swipeRefreshLayout;
    private ListView listView;
    private Handler handler = new Handler();

    @Override
    protected int setLayoutId() {
        return R.layout.fragment_my;
    }

    @Override
    protected void init() {
        swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
        listView = (ListView) findViewById(R.id.listView);
        swipeRefreshLayout.setOnRefreshListener(this);
    }

    @Override
    protected void loadData() {
        onRefresh();
    }

    @Override
    public void onRefresh() {
        // 只加載一次數(shù)據(jù),避免界面切換的時候,加載數(shù)據(jù)多次
        if (listView.getAdapter() == null) {
            swipeRefreshLayout.setRefreshing(true);
            new Thread() {
                @Override
                public void run() {
                    // 延遲1秒
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, new String[]{
                                    "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111",
                                    "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111"
                            }));
                            swipeRefreshLayout.setRefreshing(false);
                        }
                    });
                }
            }.start();
        }
    }
}

ok,這樣封裝就結(jié)束了,以后不需要懶加載Framgent可以直接繼承BaseFragment,需要懶加載的直接繼承BaseLazyLoadFragment。

擴展#

如果你也自己敲著代碼去研究懶加載,并且Framgent的數(shù)量大于2個,你會發(fā)現(xiàn)等你翻到第三個,再重新返回第一個的時候,第一個又重新加載了,并且重新走了創(chuàng)建周期,我在這里給不明白的原因的朋友解釋一下:

ViewPager只默認預(yù)加載下一頁的Fragment,其他的Fragment會被移除并銷毀。
下一次再添加的時候就需要重新創(chuàng)建Fragment。

那如果解決這個問題呢?

viewPager.setOffscreenPageLimit(2);

這個方法可以設(shè)置ViewPager預(yù)加載的頁數(shù),我們看一下方法的注釋:

/**
* Set the number of pages that should be retained to either side of the
* current page in the view hierarchy in an idle state. Pages beyond this
* limit will be recreated from the adapter when needed.
*
* <p>This is offered as an optimization. If you know in advance the number
* of pages you will need to support or have lazy-loading mechanisms in place
* on your pages, tweaking this setting can have benefits in perceived smoothness
* of paging animations and interaction. If you have a small number of pages (3-4)
* that you can keep active all at once, less time will be spent in layout for
* newly created view subtrees as the user pages back and forth.</p>
*
* <p>You should keep this limit low, especially if your pages have complex layouts.
* This setting defaults to 1.</p>
*
* @param limit How many pages will be kept offscreen in an idle state.
*/

英文注釋有點長,簡單翻譯總結(jié)一下幾點:

1、設(shè)置當前頁的左右兩邊的預(yù)加載數(shù)量。

2、通過設(shè)置預(yù)加載的數(shù)量,有利于動畫的顯示和交互。(上面的英文提到了懶加載,個人感覺跟懶加載沒什么關(guān)系,預(yù)加載數(shù)量少了,必然流暢度相對會越高)

3、如果頁數(shù)比較少(例如3到4個),可以加載所有的頁數(shù),這樣可以介紹頁數(shù)創(chuàng)建的時間,滑動更流暢

4、保持預(yù)加載個數(shù)偏小,除非你的頁數(shù)有多種復(fù)雜的布局,默認為1。(不可能小于1,方法里判斷)

了解了這個方法的特性,我們只要viewPager.setOffscreenPageLimit(2)就可以了,這樣就解決了剛才的問題。

另外要說的是setUserVisibleHint()只有在ViewPager+Fragment的時候才有效,單用Fragment的時候可以考慮使用onHiddenChanged(boolean hidden)方法。

總結(jié)#

ok,懶加載就到此結(jié)束,這個問題一家國內(nèi)大型的互聯(lián)網(wǎng)上市公司問的問題,我們平時可能覺得懶加載什么的無所謂,產(chǎn)品就是矯情,然后開始抱怨這個那個的,現(xiàn)在看來這就是差別的所在,大公司對于性能的要求真是非常的嚴格,這一點我們都需要耐心的學(xué)習(xí),才能真正的有所提高。

Demo下載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容