Android AsyncLayoutInflater 限制及改進(jìn)

本文概述

建議先回顧下之前四篇文章甩栈,這個(gè)系列的文章從前往后順序看最佳:

上一篇文章中我們介紹了 AsyncLayoutInflater 的用法及源碼實(shí)現(xiàn),那么本文來分析下 AsyncLayoutInflater 使用的注意事項(xiàng)及改進(jìn)方案嫩海。

1、注意事項(xiàng)

For a layout to be inflated asynchronously it needs to have a parent whose generateLayoutParams(AttributeSet) is thread-safe and all the Views being constructed as part of inflation must not create any Handlers or otherwise call myLooper(). If the layout that is trying to be inflated cannot be constructed asynchronously for whatever reason, AsyncLayoutInflater will automatically fall back to inflating on the UI thread.

NOTE that the inflated View hierarchy is NOT added to the parent. It is equivalent to calling inflate(int, ViewGroup, boolean) with attachToRoot set to false. Callers will likely want to call addView(View) in the AsyncLayoutInflater.OnInflateFinishedListener callback at a minimum.

This inflater does not support setting a LayoutInflater.Factory nor LayoutInflater.Factory2. Similarly it does not support inflating layouts that contain fragments.

以上來自 AsyncLayoutInflater 的說明文檔:

  1. 使用異步 inflate囚痴,那么需要這個(gè) layout 的 parent 的 generateLayoutParams 函數(shù)是線程安全的叁怪;
  2. 所有構(gòu)建的 View 中必須不能創(chuàng)建 Handler 或者是調(diào)用 Looper.myLooper;(因?yàn)槭窃诋惒骄€程中加載的深滚,異步線程默認(rèn)沒有調(diào)用 Looper.prepare )奕谭;
  3. 異步轉(zhuǎn)換出來的 View 并沒有被加到 parent view中,AsyncLayoutInflater 是調(diào)用了 LayoutInflater.inflate(int, ViewGroup, false)痴荐,因此如果需要加到 parent view 中血柳,就需要我們自己手動(dòng)添加;
  4. AsyncLayoutInflater 不支持設(shè)置 LayoutInflater.Factory 或者 LayoutInflater.Factory2生兆;
  5. 不支持加載包含 Fragment 的 layout难捌;
  6. 如果 AsyncLayoutInflater 失敗,那么會(huì)自動(dòng)回退到UI線程來加載布局鸦难;

2根吁、注意事項(xiàng)說明

以上注意事項(xiàng)2、3合蔽、6兩項(xiàng)非常容易明白击敌,下面分析下其余幾項(xiàng);

2.1 使用異步 inflate拴事,那么需要這個(gè) layout 的 parent 的 generateLayoutParams 函數(shù)是線程安全的沃斤;

我們看下 ViewGroup 中的 generateLayoutParams 方法

    /**
     * Returns a new set of layout parameters based on the supplied attributes set.
     * @param attrs the attributes to build the layout parameters from
     * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
     *         of its descendants
     */
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

generateLayoutParams 方法只是直接new了一個(gè)對(duì)象,因而非線程安全情況下創(chuàng)建多次而使用非同一個(gè)對(duì)象的情況刃宵。

2.2 AsyncLayoutInflater 不支持設(shè)置 LayoutInflater.Factory 或者 LayoutInflater.Factory2衡瓶;

這個(gè)很好解釋,因?yàn)?AsyncLayoutInflater 沒有提供類似的Api牲证,但是看過之前文章的小伙伴肯定知道這兩個(gè)類是非常關(guān)鍵的鞍陨,如果 AsyncLayoutInflater 不支持設(shè)置,那么有些情況下效果肯定是不一樣的从隆,使用了異步之后導(dǎo)致效果不一樣豈不是很坑诚撵,下面我們?cè)倬唧w解決。

2.3 不支持加載包含 Fragment 的 layout键闺;

前面的不支持三個(gè)字是不是讓你心里一涼寿烟,其實(shí)這三個(gè)字不夠準(zhǔn)確,應(yīng)該改為不完全支持辛燥。這一條要一篇文章的篇幅才能說明白筛武,我們下篇文章再說哈缝其。

3、可改進(jìn)點(diǎn)

AsyncLayoutInflater 的代碼并不多徘六,而且代碼質(zhì)量也很高内边,所以其中可以優(yōu)化的地方寥寥,簡(jiǎn)單說下我的看法:

  1. AsyncLayoutInflater 只能通過回調(diào)的方式返回真正 Inflate 出來的View待锈,但是假設(shè)一種場(chǎng)景漠其,使用 AsyncLayoutInflater 去異步加載 Layout 和使用不是同一個(gè)類;
  2. AsyncLayoutInflater 中不能 setFactory竿音,這樣通過 AsyncLayoutInflater 加載的布局是無法得到系統(tǒng)的兼容(例如 TextView 變?yōu)?AppCompatTextView);
  3. 因?yàn)橛腥蝿?wù)排隊(duì)機(jī)制和屎,那么可能出現(xiàn)需要使用時(shí)任務(wù)仍然沒有執(zhí)行的場(chǎng)景,此時(shí)等待任務(wù)被執(zhí)行還不如直接在主線程加載春瞬;

那么修改方案也很簡(jiǎn)單:

  1. 封裝 AsyncLayoutInflater柴信,修改調(diào)用方法,屏蔽不同類使用造成的影響宽气;
  2. 直接在 AsyncLayoutInflater 的 Inflater 中進(jìn)行相關(guān)設(shè)置随常;
  3. 在獲取加載出來 View 的 Api 中做判斷,如果當(dāng)前任務(wù)沒有被執(zhí)行萄涯,則直接在 UI 線程加載绪氛;

4、封裝

因?yàn)?AsyncLayoutInflater 是 final 的窃判,因而不能使用繼承,我們就將其 Copy 一份直接修改其中代碼喇闸,修改點(diǎn)就是 針對(duì)章節(jié)3中可改進(jìn)的地方袄琳。不多說,直接 Show The Code燃乍。


/**
 * 實(shí)現(xiàn)異步加載布局的功能唆樊,修改點(diǎn):
 * 
 * 1. super.onCreate之前調(diào)用沒有了默認(rèn)的Factory;
 * 2. 排隊(duì)過多的優(yōu)化刻蟹;
 */
public class AsyncLayoutInflaterPlus {

    private static final String TAG = "AsyncLayoutInflaterPlus";
    private Handler mHandler;
    private LayoutInflater mInflater;
    private InflateRunnable mInflateRunnable;
    // 真正執(zhí)行加載任務(wù)的線程池
    private static ExecutorService sExecutor = Executors.newFixedThreadPool(Math.max(2,
            Runtime.getRuntime().availableProcessors() - 2));
    // InflateRequest pool
    private static Pools.SynchronizedPool<AsyncLayoutInflaterPlus.InflateRequest> sRequestPool = new Pools.SynchronizedPool<>(10);
    private Future<?> future;

    public AsyncLayoutInflaterPlus(@NonNull Context context) {
        mInflater = new AsyncLayoutInflaterPlus.BasicInflater(context);
        mHandler = new Handler(mHandlerCallback);
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull CountDownLatch countDownLatch,
                        @NonNull AsyncLayoutInflaterPlus.OnInflateFinishedListener callback) {
        if (callback == null) {
            throw new NullPointerException("callback argument may not be null!");
        }
        AsyncLayoutInflaterPlus.InflateRequest request = obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        request.countDownLatch = countDownLatch;
        mInflateRunnable = new InflateRunnable(request);
        future = sExecutor.submit(mInflateRunnable);
    }

    public void cancel() {
        future.cancel(true);
    }

    /**
     * 判斷這個(gè)任務(wù)是否已經(jīng)開始執(zhí)行
     *
     * @return
     */
    public boolean isRunning() {
        return mInflateRunnable.isRunning();
    }

    private Handler.Callback mHandlerCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            AsyncLayoutInflaterPlus.InflateRequest request = (AsyncLayoutInflaterPlus.InflateRequest) msg.obj;
            if (request.view == null) {
                request.view = mInflater.inflate(
                        request.resid, request.parent, false);
            }
            request.callback.onInflateFinished(
                    request.view, request.resid, request.parent);
            request.countDownLatch.countDown();
            releaseRequest(request);
            return true;
        }
    };

    public interface OnInflateFinishedListener {
        void onInflateFinished(View view, int resid, ViewGroup parent);
    }

    private class InflateRunnable implements Runnable {
        private InflateRequest request;
        private boolean isRunning;

        public InflateRunnable(InflateRequest request) {
            this.request = request;
        }

        @Override
        public void run() {
            isRunning = true;
            try {
                request.view = request.inflater.mInflater.inflate(
                        request.resid, request.parent, false);
            } catch (RuntimeException ex) {
                // Probably a Looper failure, retry on the UI thread
                Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
                        + " thread", ex);
            }
            Message.obtain(request.inflater.mHandler, 0, request)
                    .sendToTarget();
        }

        public boolean isRunning() {
            return isRunning;
        }
    }

    private static class InflateRequest {
        AsyncLayoutInflaterPlus inflater;
        ViewGroup parent;
        int resid;
        View view;
        AsyncLayoutInflaterPlus.OnInflateFinishedListener callback;
        CountDownLatch countDownLatch;

        InflateRequest() {
        }
    }

    private static class BasicInflater extends LayoutInflater {
        private static final String[] sClassPrefixList = {
                "android.widget.",
                "android.webkit.",
                "android.app."
        };

        BasicInflater(Context context) {
            super(context);
            if (context instanceof AppCompatActivity) {
                // 加上這些可以保證AppCompatActivity的情況下逗旁,super.onCreate之前
                // 使用AsyncLayoutInflater加載的布局也擁有默認(rèn)的效果
                AppCompatDelegate appCompatDelegate = ((AppCompatActivity) context).getDelegate();
                if (appCompatDelegate instanceof LayoutInflater.Factory2) {
                    LayoutInflaterCompat.setFactory2(this, (LayoutInflater.Factory2) appCompatDelegate);
                }
            }
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return new AsyncLayoutInflaterPlus.BasicInflater(newContext);
        }

        @Override
        protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
            for (String prefix : sClassPrefixList) {
                try {
                    View view = createView(name, prefix, attrs);
                    if (view != null) {
                        return view;
                    }
                } catch (ClassNotFoundException e) {
                    // In this case we want to let the base class take a crack
                    // at it.
                }
            }

            return super.onCreateView(name, attrs);
        }
    }

    public AsyncLayoutInflaterPlus.InflateRequest obtainRequest() {
        AsyncLayoutInflaterPlus.InflateRequest obj = sRequestPool.acquire();
        if (obj == null) {
            obj = new AsyncLayoutInflaterPlus.InflateRequest();
        }
        return obj;
    }

    public void releaseRequest(AsyncLayoutInflaterPlus.InflateRequest obj) {
        obj.callback = null;
        obj.inflater = null;
        obj.parent = null;
        obj.resid = 0;
        obj.view = null;
        sRequestPool.release(obj);
    }

}
/**
 * 調(diào)用入口類;同時(shí)解決加載和獲取View在不同類的場(chǎng)景
 */
public class AsyncLayoutLoader {

    private int mLayoutId;
    private View mRealView;
    private Context mContext;
    private ViewGroup mRootView;
    private CountDownLatch mCountDownLatch;
    private AsyncLayoutInflaterPlus mInflater;
    private static SparseArrayCompat<AsyncLayoutLoader> sArrayCompat = new SparseArrayCompat<AsyncLayoutLoader>();

    public static AsyncLayoutLoader getInstance(Context context) {
        return new AsyncLayoutLoader(context);
    }

    private AsyncLayoutLoader(Context context) {
        this.mContext = context;
        mCountDownLatch = new CountDownLatch(1);
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent) {
        inflate(resid, parent, null);
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
                        AsyncLayoutInflaterPlus.OnInflateFinishedListener listener) {
        mRootView = parent;
        mLayoutId = resid;
        sArrayCompat.append(mLayoutId, this);
        if (listener == null) {
            listener = new AsyncLayoutInflaterPlus.OnInflateFinishedListener() {
                @Override
                public void onInflateFinished(View view, int resid, ViewGroup parent) {
                    mRealView = view;
                }
            };
        }
        mInflater = new AsyncLayoutInflaterPlus(mContext);
        mInflater.inflate(resid, parent, mCountDownLatch, listener);
    }

    /**
     * getLayoutLoader 和 getRealView 方法配對(duì)出現(xiàn)
     * 用于加載和獲取View在不同類的場(chǎng)景
     *
     * @param resid
     * @return
     */
    public static AsyncLayoutLoader getLayoutLoader(int resid) {
        return sArrayCompat.get(resid);
    }

    /**
     * getLayoutLoader 和 getRealView 方法配對(duì)出現(xiàn)
     * 用于加載和獲取View在不同類的場(chǎng)景
     *
     * @param resid
     * @return
     */
    public View getRealView() {
        if (mRealView == null && !mInflater.isRunning()) {
            mInflater.cancel();
            inflateSync();
        } else if (mRealView == null) {
            try {
                mCountDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setLayoutParamByParent(mContext, mRootView, mLayoutId, mRealView);
        } else {
            setLayoutParamByParent(mContext, mRootView, mLayoutId, mRealView);
        }
        return mRealView;
    }


    /**
     * 根據(jù)Parent設(shè)置異步加載View的LayoutParamsView
     *
     * @param context
     * @param parent
     * @param layoutResId
     * @param view
     */
    private static void setLayoutParamByParent(Context context, ViewGroup parent, int layoutResId, View view) {
        if (parent == null) {
            return;
        }
        final XmlResourceParser parser = context.getResources().getLayout(layoutResId);
        try {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            ViewGroup.LayoutParams params = parent.generateLayoutParams(attrs);
            view.setLayoutParams(params);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            parser.close();
        }
    }

    private void inflateSync() {
        mRealView = LayoutInflater.from(mContext).inflate(mLayoutId, mRootView, false);
    }

}

5舆瘪、總結(jié)

本文主要是分析 AsyncLayoutInflater 的使用注意事項(xiàng)片效,并對(duì)其中的限制進(jìn)行了改進(jìn),此處不再累述英古。

下一篇文章我們一起探究下為什么 AsyncLayoutInflater 文檔上寫不支持包含 Fragment 標(biāo)簽的異步淀衣,以及真的不能異步嗎?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末召调,一起剝皮案震驚了整個(gè)濱河市膨桥,隨后出現(xiàn)的幾起案子蛮浑,更是在濱河造成了極大的恐慌,老刑警劉巖只嚣,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沮稚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡册舞,警方通過查閱死者的電腦和手機(jī)蕴掏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來环础,“玉大人囚似,你說我怎么就攤上這事∠叩茫” “怎么了饶唤?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)贯钩。 經(jīng)常有香客問我募狂,道長(zhǎng),這世上最難降的妖魔是什么角雷? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任祸穷,我火速辦了婚禮,結(jié)果婚禮上勺三,老公的妹妹穿的比我還像新娘雷滚。我一直安慰自己,他們只是感情好吗坚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布祈远。 她就那樣靜靜地躺著,像睡著了一般商源。 火紅的嫁衣襯著肌膚如雪车份。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天牡彻,我揣著相機(jī)與錄音扫沼,去河邊找鬼。 笑死庄吼,一個(gè)胖子當(dāng)著我的面吹牛缎除,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播总寻,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼伴找,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了废菱?” 一聲冷哼從身側(cè)響起技矮,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤抖誉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后衰倦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袒炉,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年樊零,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了我磁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡驻襟,死狀恐怖夺艰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情沉衣,我是刑警寧澤郁副,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站豌习,受9級(jí)特大地震影響存谎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肥隆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一既荚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧栋艳,春花似錦恰聘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至旬昭,卻和暖如春篙螟,著一層夾襖步出監(jiān)牢的瞬間菌湃,已是汗流浹背问拘。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惧所,地道東北人骤坐。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像下愈,于是被迫代替她去往敵國(guó)和親纽绍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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

  • 一势似、適用場(chǎng)景 ListViewListview是一個(gè)很重要的組件拌夏,它以列表的形式根據(jù)數(shù)據(jù)的長(zhǎng)自適應(yīng)展示具體內(nèi)容,用...
    Geeks_Liu閱讀 10,674評(píng)論 1 28
  • 嘗試過幾次僧著,真是做不好短線操作,做個(gè)T很容易T飛障簿。且短線操作太過糾結(jié)盹愚,心太累,沒做好反而影響盤感站故。 基于對(duì)市場(chǎng)行情...
    村落時(shí)光閱讀 194評(píng)論 0 0
  • 啤酒瓶里晃蕩月色西篓, 青石板上燈光模糊了眼愈腾, 這跳動(dòng)的心怎樣安插, 樹蔭下道路坑坑洼洼岂津, 晚風(fēng)啊你來說虱黄, 這跳動(dòng)的心...
    拓叔閱讀 280評(píng)論 2 4
  • 01. “終于等到小長(zhǎng)假了,我準(zhǔn)備去西塘看看寸爆,聽說那兒很美礁鲁。”你一臉興奮赁豆,滿腦子憧憬仅醇,雙眼迷離,開始陷入對(duì)美景的向...
    胖死你個(gè)大肥貓閱讀 386評(píng)論 0 2
  • 我路過這人間 聽喜鵲說著謊言 一年又一年 這人間似陰間 地獄有無天 我掉入輪回里面 你是最大的苦難
    曉曉博士閱讀 153評(píng)論 0 2