【Android笔记】异步加载View,AsyncLayoutInflater原理

Android异步加载View:AsyncLayoutInflater详解
AsyncLayoutInflater是谷歌提供的异步加载UI工具,减少主线程消耗。本文介绍了其使用方法、初始化过程、加载原理,以及完整源码分析。

异步加载View,AsyncLayoutInflater原理

AsyncLayoutInflater是谷歌提供的一个异步加载UI方案,其可以异步加载控件并回调给UI,以此减少主线程消耗。

使用

在应用的build.gradle中添加如下依赖

...
dependencies {
    ...
    implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'
}

依赖后,可以在任何能获取context的场景调用如下方法进行异步加载

    /* KOTLIN */

    /**
     * 示例方法
     *
     * @param ctx   上下文实例
     * @param layoutRes 目标布局文件id
     * @param parent    父布局
     */
    fun sample(ctx: Context, layoutRes: Int, parent: ViewGroup) {
        AsyncLayoutInflater(ctx).inflate(layoutRes, parent) { view, id, parentView ->
            Log.d("RESULT_INTRO", "view: 目标加载布局")
            Log.d("RESULT_INTRO", "id: 目标加载布局资源id")
            Log.d("RESULT_INTRO", "view: 父布局")
            
            // 该方法仅加载布局,父布局仅用来计算尺寸,不会将目标布局放入父布局
            parentView?.addView(view)
        }
    }
    /* JAVA */
    
    /**
     * 示例方法
     *
     * @param ctx   上下文实例
     * @param layoutRes 目标布局文件id
     * @param parent    父布局
     */
    void sample(Context ctx, int layoutRes, ViewGroup parent) {
        new AsyncLayoutInflater(ctx)
                .inflate(layoutRes, parent, (view, resid, parentView) -> {
                    Log.d("RESULT_INTRO", "view: 目标加载布局");
                    Log.d("RESULT_INTRO", "resid: 目标加载布局资源id");
                    Log.d("RESULT_INTRO", "view: 父布局");

                    // 该方法仅加载布局,父布局仅用来计算尺寸,不会将目标布局放入父布局
                    if (parentView != null) parentView.addView(view);
                });
    }

该异步加载仅相当于LayoutInflater#inflate(int, ViewGroup, boolean)方法中attachToRoot参数为false的结果。由于不知道异步加载结束后父布局会不会已被回收,所以加载结束后不会直接加入父布局,仅做尺寸计算

原理

打开AsyncLayoutInflater类,可以看到该类维护了一个LayoutInflater、一个用于将在加载完成后将事件回调给原线程(创建AsyncLayoutInflater的线程)的Handler,一个用来加载layout的线程。

初始化

    // AsyncLayoutInflater.java
    ...
       public AsyncLayoutInflater(@NonNull Context context) {
        mInflater = new BasicInflater(context);
        mHandler = new Handler(mHandlerCallback);
        mInflateThread = InflateThread.getInstance();
    }
    ...

在其构造函数中将前面提到的三个变量进行了初始化。可以看出其加载线程是一个静态内部类模式的单例。该线程维护了一个存放请求的队列,并在线程中死循环去从队列取加载请求并进行加载。

    // AsyncLayoutInflater.java
    // AsyncLayoutInflater.InflateThread.class
    
    ...
        @Override
        public void run() {
            while (true) {
                runInner();
            }
        }
        
        // 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.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();
        }
    ...

在加载线程的实现中,会不断尝试从队列中获取请求。当获取到请求后则通过维护的inflater将控件加载出来。若此时发生跨线程加载问题,则说明该线程可能looper有问题,之后会在UI线程中重试,最后将结果通过handler发送出去

加载

    // AsyncLayoutInflater.java
    
    ...
    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
            @NonNull OnInflateFinishedListener callback) {
        if (callback == null) {
            throw new NullPointerException("callback argument may not be null!");
        }
        InflateRequest request = mInflateThread.obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        mInflateThread.enqueue(request);
    }

    ...

在inflate方法中,首先通过线程的obtainRequest()获取一个request实例,然后在设置好参数后将其加入加载线程的队列中。

    // AsyncLayoutInflater.java
    // AsyncLayoutInflater.InflateThread.class
    
    private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
    private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
    
    ...
         public InflateRequest obtainRequest() {
            InflateRequest obj = mRequestPool.acquire();
            if (obj == null) {
                obj = new InflateRequest();
            }
            return obj;
        }
    ...

obtainRequest()方法首先尝试从mRequestPool变量中获取,该变量是一个Pools缓冲池实例,被使用完毕的实例会在这个池中重复使用。若缓冲池为空则创建一个新的并返回。之后该请求会像前文中提到的一样被重新赋值并添加到线程的队列中,直到被线程取出并加载,最后通过handler发送出去。

最后看一下handler是如何将其发送的

    // AsyncLayoutInflater.java
    ...
    private Callback mHandlerCallback = new Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            InflateRequest request = (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);
            mInflateThread.releaseRequest(request);
            return true;
        }
    };
    ...

回调中首先判断请求中的view变量是否为空,若为空则可能是因为之前的请求线程存在looper问题,在这里重试一次,最后通过回调抛回调用方,整个异步调用流程结束

完整源码

最后放上完整的源码用来参考

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.asynclayoutinflater.view;

import android.content.Context;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.util.Pools.SynchronizedPool;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * <p>Helper class for inflating layouts asynchronously. To use, construct
 * an instance of {@link AsyncLayoutInflater} on the UI thread and call
 * {@link #inflate(int, ViewGroup, OnInflateFinishedListener)}. The
 * {@link OnInflateFinishedListener} will be invoked on the UI thread
 * when the inflate request has completed.
 *
 * <p>This is intended for parts of the UI that are created lazily or in
 * response to user interactions. This allows the UI thread to continue
 * to be responsive & animate while the relatively heavy inflate
 * is being performed.
 *
 * <p>For a layout to be inflated asynchronously it needs to have a parent
 * whose {@link ViewGroup#generateLayoutParams(AttributeSet)} is thread-safe
 * and all the Views being constructed as part of inflation must not create
 * any {@link Handler}s or otherwise call {@link Looper#myLooper()}. If the
 * layout that is trying to be inflated cannot be constructed
 * asynchronously for whatever reason, {@link AsyncLayoutInflater} will
 * automatically fall back to inflating on the UI thread.
 *
 * <p>NOTE that the inflated View hierarchy is NOT added to the parent. It is
 * equivalent to calling {@link LayoutInflater#inflate(int, ViewGroup, boolean)}
 * with attachToRoot set to false. Callers will likely want to call
 * {@link ViewGroup#addView(View)} in the {@link OnInflateFinishedListener}
 * callback at a minimum.
 *
 * <p>This inflater does not support setting a {@link LayoutInflater.Factory}
 * nor {@link LayoutInflater.Factory2}. Similarly it does not support inflating
 * layouts that contain fragments.
 */
public final class AsyncLayoutInflater {
    private static final String TAG = "AsyncLayoutInflater";

    LayoutInflater mInflater;
    Handler mHandler;
    InflateThread mInflateThread;

    public AsyncLayoutInflater(@NonNull Context context) {
        mInflater = new BasicInflater(context);
        mHandler = new Handler(mHandlerCallback);
        mInflateThread = InflateThread.getInstance();
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
            @NonNull OnInflateFinishedListener callback) {
        if (callback == null) {
            throw new NullPointerException("callback argument may not be null!");
        }
        InflateRequest request = mInflateThread.obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        mInflateThread.enqueue(request);
    }

    private Callback mHandlerCallback = new Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            InflateRequest request = (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);
            mInflateThread.releaseRequest(request);
            return true;
        }
    };

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

    private static class InflateRequest {
        AsyncLayoutInflater inflater;
        ViewGroup parent;
        int resid;
        View view;
        OnInflateFinishedListener callback;

        InflateRequest() {
        }
    }

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

        BasicInflater(Context context) {
            super(context);
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return new 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);
        }
    }

    private static class InflateThread extends Thread {
        private static final InflateThread sInstance;
        static {
            sInstance = new InflateThread();
            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.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();
        }

        @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.inflater = null;
            obj.parent = null;
            obj.resid = 0;
            obj.view = 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);
            }
        }
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值