Android AsyncLayoutInflater 限制及改進

貌似許亞軍發表於2018-08-13

本文概述

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

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

1、注意事項

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,那麼需要這個 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執行緒來載入佈局;

2、注意事項說明

以上注意事項2、3、6兩項非常容易明白,下面分析下其餘幾項;

2.1 使用非同步 inflate,那麼需要這個 layout 的 parent 的 generateLayoutParams 函式是執行緒安全的;

我們看下 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了一個物件,因而非執行緒安全情況下建立多次而使用非同一個物件的情況。

2.2 AsyncLayoutInflater 不支援設定 LayoutInflater.Factory 或者 LayoutInflater.Factory2;

這個很好解釋,因為 AsyncLayoutInflater 沒有提供類似的Api,但是看過之前文章的小夥伴肯定知道這兩個類是非常關鍵的,如果 AsyncLayoutInflater 不支援設定,那麼有些情況下效果肯定是不一樣的,使用了非同步之後導致效果不一樣豈不是很坑,下面我們再具體解決。

2.3 不支援載入包含 Fragment 的 layout;

前面的不支援三個字是不是讓你心裡一涼,其實這三個字不夠準確,應該改為不完全支援。這一條要一篇文章的篇幅才能說明白,我們下篇文章再說哈。

3、可改進點

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 執行緒載入;

4、封裝

因為 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);
    }

}
複製程式碼

5、總結

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

下一篇文章我們一起探究下為什麼 AsyncLayoutInflater 文件上寫不支援包含 Fragment 標籤的非同步,以及真的不能非同步嗎?

相關文章