32

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

 4 years ago
source link: https://juejin.im/post/5dd499a6f265da0bf21126cc
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

系列文章索引

Android系统启动流程

LayoutInflater源码详解

录播回放已上传,请戳链接食用:【Android/源码/面试】LayoutInflater源码详解

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

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

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

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

整体流程概览

1

彩蛋:BlinkLayout

BlinkLayout是LayoutInflater中的一个内部类,它本身是是FrameLayout的子类,如果当前标签为TAG_1995,则创建一个隔0.5秒闪烁一次的BlinkLayout来承载它的布局内容

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

LayoutInflater

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中光标的闪烁效果

16e86998404438a3?imageslim

扫描上方二维码关注「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研习社所有,未经允许禁止转载,侵权必究!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK