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

前言#

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

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

目前大部分的app都使用Fragment懶加載機(jī)制捡鱼,例如嗶哩嗶哩八回,360手機(jī)助手等等。

正文#

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

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

首先我們看看這個(gè)方法的注釋:

/**
* 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.
*/

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

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

這里寫圖片描述

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

然后滑動(dòng)ViewPager念脯,看一下運(yùn)行情況:

這里寫圖片描述

滑動(dòng)的時(shí)候狞洋,兩個(gè)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)建時(shí)要判斷是否已經(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í)候,加載數(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();
        }
    }
}

效果圖我就不貼出來了转培,最后會(huì)有源碼鏈接恶导,大家可以下載運(yùn)行看看效果。

這樣就結(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)建時(shí)要判斷是否已經(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í)候,加載數(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恕汇。

擴(kuò)展#

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

ViewPager只默認(rèn)預(yù)加載下一頁的Fragment但惶,其他的Fragment會(huì)被移除并銷毀。
下一次再添加的時(shí)候就需要重新創(chuàng)建Fragment湿蛔。

那如果解決這個(gè)問題呢膀曾?

viewPager.setOffscreenPageLimit(2);

這個(gè)方法可以設(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.
*/

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

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

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

3栗弟、如果頁數(shù)比較少(例如3到4個(gè))污筷,可以加載所有的頁數(shù),這樣可以介紹頁數(shù)創(chuàng)建的時(shí)間乍赫,滑動(dòng)更流暢

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

了解了這個(gè)方法的特性改鲫,我們只要viewPager.setOffscreenPageLimit(2)就可以了诈皿,這樣就解決了剛才的問題林束。

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

總結(jié)#

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

Demo下載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市壳嚎,隨后出現(xiàn)的幾起案子桐智,更是在濱河造成了極大的恐慌,老刑警劉巖烟馅,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酵使,死亡現(xiàn)場離奇詭異,居然都是意外死亡焙糟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門样屠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來穿撮,“玉大人,你說我怎么就攤上這事痪欲≡么” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵业踢,是天一觀的道長栗柒。 經(jīng)常有香客問我,道長知举,這世上最難降的妖魔是什么瞬沦? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮雇锡,結(jié)果婚禮上逛钻,老公的妹妹穿的比我還像新娘。我一直安慰自己锰提,他們只是感情好曙痘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布芳悲。 她就那樣靜靜地躺著,像睡著了一般边坤。 火紅的嫁衣襯著肌膚如雪名扛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天茧痒,我揣著相機(jī)與錄音肮韧,去河邊找鬼。 笑死文黎,一個(gè)胖子當(dāng)著我的面吹牛岛宦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播泽裳,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼蝉稳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了劳闹?” 一聲冷哼從身側(cè)響起院究,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎本涕,沒想到半個(gè)月后业汰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菩颖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年样漆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晦闰。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡放祟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呻右,到底是詐尸還是另有隱情跪妥,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布声滥,位于F島的核電站眉撵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏落塑。R本人自食惡果不足惜纽疟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望憾赁。 院中可真熱鬧仰挣,春花似錦、人聲如沸缠沈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至颓芭,卻和暖如春顷锰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背亡问。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工官紫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人州藕。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓束世,卻偏偏與公主長得像,于是被迫代替她去往敵國和親床玻。 傳聞我的和親對象是個(gè)殘疾皇子毁涉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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