Android AsyncLayoutInflater 限制及改進

本文概述

建議先回顧下以前四篇文章,這個系列的文章從前日後順序看最佳:android

上一篇文章中咱們介紹了 AsyncLayoutInflater 的用法及源碼實現,那麼本文來分析下 AsyncLayoutInflater 使用的注意事項及改進方案。web

一、注意事項

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.bash

以上來自 AsyncLayoutInflater 的說明文檔:微信

  1. 使用異步 inflate,那麼須要這個 layout 的 parent 的 generateLayoutParams 函數是線程安全的;
  2. 全部構建的 View 中必須不能建立 Handler 或者是調用 Looper.myLooper;(由於是在異步線程中加載的,異步線程默認沒有調用 Looper.prepare );
  3. 異步轉換出來的 View 並無被加到 parent view中,AsyncLayoutInflater 是調用了 LayoutInflater.inflate(int, ViewGroup, false),所以若是須要加到 parent view 中,就須要咱們本身手動添加;
  4. AsyncLayoutInflater 不支持設置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;
  5. 不支持加載包含 Fragment 的 layout;
  6. 若是 AsyncLayoutInflater 失敗,那麼會自動回退到UI線程來加載佈局;

二、注意事項說明

以上注意事項二、三、6兩項很是容易明白,下面分析下其他幾項;併發

2.1 使用異步 inflate,那麼須要這個 layout 的 parent 的 generateLayoutParams 函數是線程安全的;

咱們看下 ViewGroup 中的 generateLayoutParams 方法app

/**
     * 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了一個對象,於是非線程安全狀況下建立屢次而使用非同一個對象的狀況。異步

2.2 AsyncLayoutInflater 不支持設置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;

這個很好解釋,由於 AsyncLayoutInflater 沒有提供相似的Api,可是看過以前文章的小夥伴確定知道這兩個類是很是關鍵的,若是 AsyncLayoutInflater 不支持設置,那麼有些狀況下效果確定是不同的,使用了異步以後致使效果不同豈不是很坑,下面咱們再具體解決。async

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

前面的不支持三個字是否是讓你內心一涼,其實這三個字不夠準確,應該改成不徹底支持。這一條要一篇文章的篇幅才能說明白,咱們下篇文章再說哈。

三、可改進點

AsyncLayoutInflater 的代碼並很少,並且代碼質量也很高,因此其中能夠優化的地方寥寥,簡單說下個人見解:

  1. InflateThread 使用單線程來作所有的 Inflate 工做,若是一個界面中 Layout 不少不必定能知足需求;同時緩存隊列默認 10 的大小限制若是超過了10個則會致使主線程的等待;
  2. AsyncLayoutInflater 只能經過回調的方式返回真正 Inflate 出來的View,可是假設一種場景,使用 AsyncLayoutInflater 去異步加載 Layout 和使用不是同一個類;
  3. AsyncLayoutInflater 中不能 setFactory,這樣經過 AsyncLayoutInflater 加載的佈局是沒法獲得系統的兼容(例如 TextView 變爲 AppCompatTextView);
  4. 由於有任務排隊機制,那麼可能出現須要使用時任務仍然沒有執行的場景,此時等待任務被執行還不如直接在主線程加載;

那麼修改方案也很簡單:

  1. 引入線程池,多個線程併發;
  2. 封裝 AsyncLayoutInflater,修改調用方法,屏蔽不一樣類使用形成的影響;
  3. 直接在 AsyncLayoutInflater 的 Inflater 中進行相關設置;
  4. 在獲取加載出來 View 的 Api 中作判斷,若是當前任務沒有被執行,則直接在 UI 線程加載;

四、封裝

由於 AsyncLayoutInflater 是 final 的,於是不能使用繼承,咱們就將其 Copy 一份直接修改其中代碼,修改點就是 針對章節3中可改進的地方。很少說,直接 Show The Code。

/**
 * 實現異步加載佈局的功能,修改點:
 * 1. 單一線程;
 * 2. super.onCreate以前調用沒有了默認的Factory;
 * 3. 排隊過多的優化;
 */
public class AsyncLayoutInflaterPlus {

    private static final String TAG = "AsyncLayoutInflaterPlus";
    private Handler mHandler;
    private LayoutInflater mInflater;
    private InflateRunnable mInflateRunnable;
    // 真正執行加載任務的線程池
    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);
    }

    /**
     * 判斷這個任務是否已經開始執行
     *
     * @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加載的佈局也擁有默認的效果
                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);
    }

}
複製代碼
/**
 * 調用入口類;同時解決加載和獲取View在不一樣類的場景
 */
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 方法配對出現
     * 用於加載和獲取View在不一樣類的場景
     *
     * @param resid
     * @return
     */
    public static AsyncLayoutLoader getLayoutLoader(int resid) {
        return sArrayCompat.get(resid);
    }

    /**
     * getLayoutLoader 和 getRealView 方法配對出現
     * 用於加載和獲取View在不一樣類的場景
     *
     * @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;
    }


    /**
     * 根據Parent設置異步加載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);
    }

}
複製代碼

五、總結

本文主要是分析 AsyncLayoutInflater 的使用注意事項,並對其中的限制進行了改進,此處再也不累述。

下一篇文章咱們一塊兒探究下爲何 AsyncLayoutInflater 文檔上寫不支持包含 Fragment 標籤的異步,以及真的不能異步嗎?

廣告時間

今日頭條各Android客戶端團隊招人火爆進行中,各個級別和應屆實習生都須要,業務增加快、日活高、挑戰大、待遇給力,各位大佬走過路過千萬不要錯過!

本科以上學歷、非頻繁跳槽(如兩年兩跳),歡迎加個人微信詳聊:KOBE8242011

歡迎關注
相關文章
相關標籤/搜索