建議先回顧下之前四篇文章甩栈,這個(gè)系列的文章從前往后順序看最佳:
- 《Android setContentView 源碼解析》;
- 《Android LayoutInflater 源碼解析》浇揩;
- 《Android LayoutInflater Factory 源碼解析》掂为;
- 《Android AsyncLayoutInflater 源碼解析》;
上一篇文章中我們介紹了 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 的說明文檔:
- 使用異步 inflate囚痴,那么需要這個(gè) layout 的 parent 的 generateLayoutParams 函數(shù)是線程安全的叁怪;
- 所有構(gòu)建的 View 中必須不能創(chuàng)建 Handler 或者是調(diào)用 Looper.myLooper;(因?yàn)槭窃诋惒骄€程中加載的深滚,異步線程默認(rèn)沒有調(diào)用 Looper.prepare )奕谭;
- 異步轉(zhuǎn)換出來的 View 并沒有被加到 parent view中,AsyncLayoutInflater 是調(diào)用了 LayoutInflater.inflate(int, ViewGroup, false)痴荐,因此如果需要加到 parent view 中血柳,就需要我們自己手動(dòng)添加;
- AsyncLayoutInflater 不支持設(shè)置 LayoutInflater.Factory 或者 LayoutInflater.Factory2生兆;
- 不支持加載包含 Fragment 的 layout难捌;
- 如果 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)單說下我的看法:
- AsyncLayoutInflater 只能通過回調(diào)的方式返回真正 Inflate 出來的View待锈,但是假設(shè)一種場(chǎng)景漠其,使用 AsyncLayoutInflater 去異步加載 Layout 和使用不是同一個(gè)類;
- AsyncLayoutInflater 中不能 setFactory竿音,這樣通過 AsyncLayoutInflater 加載的布局是無法得到系統(tǒng)的兼容(例如 TextView 變?yōu)?AppCompatTextView);
- 因?yàn)橛腥蝿?wù)排隊(duì)機(jī)制和屎,那么可能出現(xiàn)需要使用時(shí)任務(wù)仍然沒有執(zhí)行的場(chǎng)景,此時(shí)等待任務(wù)被執(zhí)行還不如直接在主線程加載春瞬;
那么修改方案也很簡(jiǎn)單:
- 封裝 AsyncLayoutInflater柴信,修改調(diào)用方法,屏蔽不同類使用造成的影響宽气;
- 直接在 AsyncLayoutInflater 的 Inflater 中進(jìn)行相關(guān)設(shè)置随常;
- 在獲取加載出來 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)簽的異步淀衣,以及真的不能異步嗎?