「Android10源码分析」为何复杂布局会产生卡顿?-- LayoutInflater详解

LayoutInflater源码详解

前言html

这篇文章会从源码的角度分析,LayoutInflater将xml文件实例化为一个view对象的流程java

咱们会发现,其中有两个部分是耗时的主要来源android

  1. XmlResourseParser对xml的遍历
  2. 反射建立View对象致使的耗时

这两点,又跟Xml的复杂程度成正相关,Xml越复杂,则递归调用所消耗的时间就越长,就产生了咱们所说的,卡顿问题git

总体流程概览

彩蛋:BlinkLayout

BlinkLayout是LayoutInflater中的一个内部类,它自己是是FrameLayout的子类,若是当前标签为TAG_1995,则建立一个隔0.5秒闪烁一次的BlinkLayout来承载它的布局内容github

源码注释也颇有意思,写了Let's party like it's 1995!, 听说是为了庆祝1995年的复活节bash

LayoutInflaterapp

public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995! 
            return new BlinkLayout(context, attrs);
        }
				...
        return view;
    }
复制代码

具体使用也很简单框架

<blink
        android:layout_below="@id/iv_qr_code"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Android研习社"
            android:textColor="#157686"
            android:textSize="55sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </blink>
复制代码

效果以下,这种效果也适合来作EditText中光标的闪烁效果ide

扫描上方二维码关注「Android研习社」公众号,获取更多学习资料!函数

ps: 想深刻学习的都关注了,还不赶快关注一波?

LayoutInflater的建立

概览

LayoutInflater是一个抽象类,它的建立,并非交由App层处理的,而是调用了from()的静态函数,经由系统服务LAYOUT_INFLATER_SERVICE,最终建立了一个LayoutInflater的子类对象--PhoneLayoutInflater

重要函数解析

LayoutInflater.from(cxt)

这个函数比较简单,就是根据传递过来的Context对象,调用getSystemService()来获取对应的系统服务,并赋值给LayoutInflater

public static LayoutInflater from(Context context) { 
        LayoutInflater LayoutInflater =  //LayoutInflate是一个系统服务,最终返回的是`PhoneLahyoutInflater`
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }
复制代码

Context自己是一个抽象类,它真正的实例化对象,是ContextImpl, 在这个类的getSystemService()函数中,真正执行获取系统服务的类,是SystemServiceRegistry,其中又封装了一个ServiceFetcher来获取真正的系统服务,全部的系统服务,都是存储在一个map集合--SYSTEM_SERVICE_FETCHERS当中,这里实际上是一个get方法,是从这个map集合中取出对应的系统服务

LayoutInflater

@Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

复制代码

SystemServiceRegistry

/** * Gets a system service from a given context. */
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
复制代码

关于对应的服务的添加,也就是调用了SYSTEM_SERVICE_FETCHERS的put函数,这个动做是交由registerService()来完成的

/** * Statically registers a system service with the context. * This method must be called during static initialization only. */
    private static <T> void registerService(String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }
复制代码

SystemServiceRegistry这个类中有一个静态代码块,是用来完成全部服务的注册的,这里咱们只关心LAYOUT_INFLATER_SERVICE对应的服务是如何注册的

static{
				...
				registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
        ...
}
复制代码

正如咱们以前所说,这里最终是建立了一个PhoneLayoutInflater并返回的,到这里LayoutInflater的建立流程就分析完了

思考

为何要交由系统服务来作,而不是直接建立一个PhoneLayoutInflater的实例对象?

LayoutInflater布局的实例化

总体流程

实例化的调用流程咱们都很熟悉了,调用layoutInflaterinflater()函数,传入一个xml的resId参数就能够了

重要函数解析

inflate

这个函数就是咱们把Xml布局文件实例化为一个View对象的入口了

LayoutInflater

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot); //这段代码实际上是必然返回null的,由于当前版本写死了预编译的Enable为false
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource); //获取XmlBlock.Parser对象
        try {
            return inflate(parser, root, attachToRoot); 
        } finally {
            parser.close(); 
        }
    }
复制代码

此处又调用了inflate(parser, root, attachToRoot)这个函数,来对Xml布局进行解析

这里看到对一些熟悉的标签,好比include,merge,的处理,具体细节请看下面的源码及注释

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {//XmlPullParser是一个接口
		//此函数是真正执行将xml解析为视图view的过程,此处的parser为根据xml布局获取到的parser对象
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final inflateAttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root; //须要返回的view对象

            try {
                advanceToRootNode(parser); //对START_TAG和END_TAG进行判断和处理
                final String name = parser.getName();  //获取当前标签

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) { //若是使用merge标签
                    if (root == null || !attachToRoot) { //使用merge标签必须有父布局,且依赖于父布局加载,不然就会抛出异常
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);//递归(Recursive)生成布局视图
                } else { //若是不使用merge标签,建立tmp 做为临时的根节点,并最终赋值给result返回
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs); ////根据标签名建立一个view

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {  //若是rootView不为空
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);  //根据rootView生成layoutparams
                        if (!attachToRoot) { //若是attachToRoot为false
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);  //设置一个临时的params
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) { //若是root不为空,且attachToRoot为true
                        root.addView(temp, params); //把temp添加到到rootview中
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) { // 若是root为空且attachToRoot为false
                        result = temp; //将temp,也就是根结点的View赋值给result
                    }
                }

            } 
						...
            return result;  //返回结果
		}
  }
复制代码

rInflate

从上面的代码中咱们也能够看到,无论是merge标签,仍是非merge标签,最终都会调用到rInflate()这个函数,这个是用于递归向下遍历xml布局,最终调用createViewFromTag()函数来反射建立View对象

具体细节请看下面的源码及注释

void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) { //若是需REQUEST_FOCUS标签
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) { //若是是“tag”标签
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) { //若是是 Include标签
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs); //对 include标签进行解析
            } else if (TAG_MERGE.equals(name)) { //若是是merge标签
                throw new InflateException("<merge /> must be the root element"); //直接抛出异常
            } else { //其余标签
                final View view = createViewFromTag(parent, name, context, attrs); //根据Tag建立view,反射建立View
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true); //递归调用rInflate函数
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }
复制代码

createViewFromTag()

终于到了咱们的重头戏,也是真正根据解析到的Tag标签去反射建立View的函数

LayoutInflater

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        try {
            View view = tryCreateView(parent, name, context, attrs); //尝试使用Factory来闯将View对象

            if (view == null) { //若是tryCreateView返回null
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
					//sample:com.aiwinn.base.widget.CameraSurfaceView
                    if (-1 == name.indexOf('.')) {  //若是当前Tag含有“.”
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } 
  			...
    }
复制代码

在这个函数中会首先调用tryCreateView()来获取View对象,若是为null,则进一步调用createView()函数来建立View对象

public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException {
				//根据Tag反射建立view
        Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);//把prefix和name进行拼接,获取到对应的Class对象

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);//获取构构造器对象
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                ...
            }

            Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = viewContext;
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            try {
                final View view = constructor.newInstance(args); //根据获取到的构造器建立一个View的实例化对象
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }catch{
              	...
            }
        } 
    }
复制代码

这里的代码其实在耗时上算是比较重量级了,由于是使用反射来建立的,通常的说法是,反射比直接建立对象要慢3倍,iReaderx2c框架就是基于这一点去作的优化

加餐

Resources的建立和获取

这里先获取到Resources对象--mResources,这个对象的建立是由createResources()来完成的,这里最终是交由ResourcesManager这个类来获取对应的resources

ContextImpl

private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName, int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
        final String[] splitResDirs;
        final ClassLoader classLoader;
        try {
            splitResDirs = pi.getSplitPaths(splitName);
            classLoader = pi.getSplitClassLoader(splitName);
        } catch (NameNotFoundException e) {
            throw new RuntimeException(e);
        }
        return ResourcesManager.getInstance().getResources(activityToken,
                pi.getResDir(),
                splitResDirs,
                pi.getOverlayDirs(),
                pi.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfig,
                compatInfo,
                classLoader);
    }
复制代码

ResourcesManager

public @Nullable Resources getResources(@Nullable IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] overlayDirs, @Nullable String[] libDirs, int displayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }
复制代码

ContextImpl

@Override
    public Resources getResources() {
        return mResources;
    }
复制代码

由这里咱们也能够推断,LayoutInflater交由服务来建立来建立,是由于其须要获取系统服务才能获取的某些资源

XmlBlock

inflate()函数里还涉及到一个重要的类, XmlResourceParser,这个类是负责对xml的标签进行遍历解析的,它的真正的实现类是XmlBlock的内部类XmlBlock.Parser,而真正完成xml的遍历操做的函数都是由XmlBlock来实现的,为了提高效率,该函数都是经过JNI调用native的函数来作的,对应的native层是android_util_XmlBlock.cpp

XmlBlock.java

@FastNative
    /*package*/ static final native int nativeNext(long state);
    @FastNative
    private static final native int nativeGetNamespace(long state);
    @FastNative
    /*package*/ static final native int nativeGetName(long state);
    @FastNative
    private static final native int nativeGetText(long state);
    @FastNative
    private static final native int nativeGetLineNumber(long state);
   	...
复制代码

``android_util_XmlBlock.cpp`

static jint android_content_XmlBlock_nativeNext(JNIEnv* env, jobject clazz, jlong token) {
    ResXMLParser* st = reinterpret_cast<ResXMLParser*>(token);
    if (st == NULL) {
        return ResXMLParser::END_DOCUMENT;
    }

    do {
        ResXMLParser::event_code_t code = st->next();
        switch (code) {
            case ResXMLParser::START_TAG:
                return 2;
            case ResXMLParser::END_TAG:
                return 3;
            case ResXMLParser::TEXT:
                return 4;
            case ResXMLParser::START_DOCUMENT:
                return 0;
            case ResXMLParser::END_DOCUMENT:
                return 1;
            case ResXMLParser::BAD_DOCUMENT:
                goto bad;
            default:
                break;
        }
    } while (true);

bad:
    jniThrowException(env, "org/xmlpull/v1/XmlPullParserException",
            "Corrupt XML binary file");
    return ResXMLParser::BAD_DOCUMENT;
}
复制代码

tryInflatePrecompiled

这个函数是Android10的源码里面新增的一个函数,是用来根据xml预编译生成的dex,经过反射来生成对应的View,从而减小XmlPullParser解析Xml的时间 -- 放到编译期来进行-- 的一个优化 ,而反射获取对应的View时能够直接获取到预编译的View对象,而不须要递归调用rInflate

这里基本上就是真正完全解决了复杂布局致使的卡顿问题

View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root, boolean attachToRoot) {
        if (!mUseCompiledView) {
            return null;
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");

        // Try to inflate using a precompiled layout.
        String pkg = res.getResourcePackageName(resource);
        String layout = res.getResourceEntryName(resource);
		
		//依然是经过反射的方式,根据已经建立的mPrecompiledClassLoader来反射生成view对象
        try {
            Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader); //获取到预编译生成的view对象的Class类
            Method inflater = clazz.getMethod(layout, Context.class, int.class);
            View view = (View) inflater.invoke(null, mContext, resource);

            if (view != null && root != null) {
                // We were able to use the precompiled inflater, but now we need to do some work to
                // attach the view to the root correctly.
                XmlResourceParser parser = res.getLayout(resource);
                try {
                    AttributeSet attrs = Xml.asAttributeSet(parser);
                    advanceToRootNode(parser);
                    ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);

                    if (attachToRoot) {
                        root.addView(view, params);
                    } else {
                        view.setLayoutParams(params);
                    }
                } finally {
                    parser.close();
                }
            }
            return view;
        } catch (Throwable e) {
            if (DEBUG) {
                Log.e(TAG, "Failed to use precompiled view", e);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return null;
    }
复制代码

写在最后

下一篇文章,咱们会提出一些优化方案,来解决(或者说)减缓复杂布局产生的卡顿问题,敬请期待!

参考文章:

1. https://www.reddit.com/r/androiddev/comments/3sekn8/lets_party_like_its_1995_from_the_layoutinflater/
2. https://www.cnblogs.com/liyilin-jack/p/10282385.html
3. https://blog.csdn.net/axi295309066/article/details/60128009
4. https://github.com/RTFSC-Android/RTFSC/blob/master/BlinkLayout.md
5. https://juejin.im/post/5c789b0ce51d454fbd5a8baa
复制代码

因为研究过程当中并未记录全部参考文章,若有疏漏,还请私聊,谢谢

郑重声明

本文版权归Android研习社全部,未经容许禁止转载,侵权必究!

相关文章
相关标签/搜索