使用 AsyncLayoutInflater 预加载xml 提高页面渲染速度

708 阅读3分钟

AsyncLayoutInflater 能否提高页面渲染速度?

答案是不能的,至少大部分情况不能。 源于默认的 AsyncLayoutInflater 使用的子线程没有指定优先级

private static class InflateThread extends Thread {
    private static final InflateThread sInstance;

    static {
        sInstance = new InflateThread();
        sInstance.setName("AsyncLayoutInflator");
        sInstance.start();
    }

    public static InflateThread getInstance() {
        return sInstance;
    }

    private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
    private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);

    // Extracted to its own method to ensure locals have a constrained liveness
    // scope by the GC. This is needed to avoid keeping previous request references
    // alive for an indeterminate amount of time, see b/33158143 for details
    public void runInner() {
        InflateRequest request;
        try {
            request = mQueue.take();
        } catch (InterruptedException ex) {
            // Odd, just continue
            Log.w(TAG, ex);
            return;
        }

        try {
            request.view = request.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);
        }

        // Trigger callback on bg thread if async inflation was successful.
        if (request.view != null && request.mExecutor != null) {
            request.mExecutor.execute(() -> triggerCallbacks(request, this));
        } else {
            Message.obtain(request.mHandler, 0, request).sendToTarget();
        }
    }

    @Override
    public void run() {
        while (true) {
            runInner();
        }
    }

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

    public void releaseRequest(InflateRequest obj) {
        obj.callback = null;
        obj.mInflater = null;
        obj.mHandler = null;
        obj.parent = null;
        obj.resid = 0;
        obj.view = null;
        obj.mExecutor = null;
        mRequestPool.release(obj);
    }

    public void enqueue(InflateRequest request) {
        try {
            mQueue.put(request);
        } catch (InterruptedException e) {
            throw new RuntimeException("Failed to enqueue async inflate request", e);
        }
    }
}

在我们默认主线程xml 加载中,主线程的优先级是-20, 而子线程除非特殊指定 否则优先级是0

那做同样的事 明显主线程会更快,既然无法 提高页面加载速度,为啥google 还要搞这么个东西出来?

主要是为了子线程去加载xml这个功能,这样一定程度上能避免anr 主线程卡顿 之类的 故障

AsyncLayoutInflater 预加载xml 思路是什么?

虽然AsyncLayoutInflater 正常使用的时候 无法对页面进行提速, 但是我们可以稍微改造一下 ,让他做到加速的效果,举个例子 一般app 启动之后 首页都是4-5个tab,默认肯定是显示第一个tab,

此时我们可以找机会 使用AsyncLayoutInflater 去提前把 其余几个tab的xml文件 都预加载一下 转成view, 这样等到真正使用的时候 可以判断一下 有没有对应的view,如果有 就直接使用view, 没有就再正常的走inflater 即可

一般情况下 我们设计一个map结构即可, key就是xml, value 就是对应的view

方案实施具体细节

对于 大部分情况下 AsyncLayoutInflater 加载出来的view 直接存入 map 中就可以使用了,只有少数情况需要特殊处理,例如一些view 需要使用application的context 来初始化的时候 就要小心了

第一个问题是 我们用application初始化后的view 到真正使用的时候 需要把这些view的 context 设置成对应activity的context , 这里需要用到反射

@JvmStatic
fun View.resetContext(context: Activity) {
    updateContext(this, context)
    val viewGroup = this as? android.view.ViewGroup ?: return
    for (i in 0 until viewGroup.childCount) {
        viewGroup.getChildAt(i).resetContext(context)
    }
}
private val contextField by lazy {
    View::class.java.getDeclaredField("mContext").also {
        it.isAccessible = true
    }
}

private fun updateContext(view: View, context: Context) {
    contextField.set(view, context)
}

其实就是遍历下view 树 重新设置context, 这里要注意的是 如果有自定义view,需要确保自定义view中 没有自己定义context 否则会有问题,举个例子

假设有个自定义view(不规范的写法)

class CustomView extends View{
  private Context mContext;// 或者其他名字
  // 这个mContext 初始化在view的构造函数里,这里略过
}

这个时候我们反射的时候 是不会替换这个mContext的,我们替换的是View下的mContext

这会导致这个CustomView的context 还是application的context,这就很危险。

要尽量避免这种不规范的自定义view 写法,其实直接getContext函数就可以解决了

第二个问题是 部分view 的初始化依赖 部分activity的theme

这种我们可以转一下即可,提前把theme参数指定好,用ContextThemeWrapper包裹一下即可

@JvmStatic
fun Context.withTheme(
    @StyleRes themeResId: Int
): ContextThemeWrapper {
    return ContextThemeWrapper(this, themeResId)
}