Android LayoutInflater inflate过程(插件化换肤原理)

Android插件化换肤原理

1.Android View的加载流程分析

Activity的view都是通过setContentView来实现组件的显示,可以用过源码来开一下Android实现XML布局文件到界面显示的

XXActivity

setContentView

AppCompatActivity

@Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

getDelegate()

@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        if (Build.VERSION.SDK_INT >= 24) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else {
            return new AppCompatDelegateImplV14(context, window, callback);
        }
    }

通过一系列的版本兼容适配,最终调用

AppCompatDelegateImplV14中的setContentView,改步骤中创建了DecorView,并使用LayoutInflater将布局文件加载到contentParent(也就是和DecorView绑定的contentParent中)

 @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

所以主要看LayoutInflater的inflate方法,这里主要创建了XML解析器

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);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

接着往下走调用inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 方法:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                advanceToRootNode(parser);
                final String name = parser.getName();

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException(" can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    //标识一 创建xml布局的父容器View
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

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

                    //标识二:创建xml中父容器包括的子View
                    // 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.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

标识一:创建xml布局的父容器View

createViewFromTag(root, name, inflaterContext, attrs);

标识二:创建xml中父容器包括的子View

rInflateChildren(parser, temp, attrs, true);

使用递归的方式调用createViewFromTag方法完成字View的加载

我们接着看createViewFromTag:

@UnsupportedAppUsage
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {

        try {
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

tryCreateView方法:

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);
    }

    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    return view;
}

首先尝试用3个Fractory创建View,如果成功就直接返回了。注意,我们可以利用这个机制,创建自己的Factory来控制View的创建过程。

如果没有Factory或创建失败,那么走默认逻辑。

先判断name中是否有'.'字符,如果没有,则认为使用android自己的View,此时会在name的前面加上包名"android.view.";如果有这个'.',则认为使用的自定义View,这时无需添加任何前缀,认为name已经包含全包名了。

最终,使用这个全包名的name来创建实例,


    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor constructor = sConstructorMap.get(name);
        Class clazz = null;
           ......

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object[] args = mConstructorArgs;
            args[1] = attrs;
            return constructor.newInstance(args);
            ......
    }

sConstructorMap缓存了View的构造方法,如果已经加载过,则直接从缓存使用构造方法创建View的实例。没有的化使用ClassLoader反射得到View的构造器

最后通过构造器使用反射,调用了View的两个构造方法反射完成View的创建。

最后将创建完的View执行AddView将视图添加到到DecorView中。

2.res的管理

我们先看Resources获取res值是如何获取的:

Resources中getColor

public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
    final TypedValue value = obtainTempTypedValue();
    try {
        final ResourcesImpl impl = mResourcesImpl;
        impl.getValue(id, value, true);
        if (value.type >= TypedValue.TYPE_FIRST_INT
                && value.type <= TypedValue.TYPE_LAST_INT) {
            return value.data;
        } else if (value.type != TypedValue.TYPE_STRING) {
            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                    + " type #0x" + Integer.toHexString(value.type) + " is not valid");
        }

        final ColorStateList csl = impl.loadColorStateList(this, value, id, theme);
        return csl.getDefaultColor();
    } finally {
        releaseTempTypedValue(value);
    }
}

ResourcesImpl

void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
        throws NotFoundException {
    boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
    if (found) {
        return;
    }
    throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}

最后通过AssetManager获取相应的值,其类型也是如此

所以我们可查看AssetManager如何创建:

在performLaunchActivity方法中,创建了Application实例

Application app = r.packageInfo.makeApplication(false, mInstrumentation);

走到LoadedApk中,创建了appContext:

ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
        cl, appClass, appContext);

使用 ContextImpl调用了createAppContext,使用 context.setResources设置了App的Resources实例对象

static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
        String opPackageName) {
    if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
    ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
            null, opPackageName);
    context.setResources(packageInfo.getResources());
    return context;
}

回到LoadedApk中getResources:

public Resources getResources() {
    if (mResources == null) {
        final String[] splitPaths;
        try {
            splitPaths = getSplitPaths(null);
        } catch (NameNotFoundException e) {
            // This should never fail.
            throw new AssertionError("null split not found");
        }

        mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                getClassLoader());
    }
    return mResources;
}

继续往下走,使用ResourcesManager获取Resources

getOrCreateResources
createResourcesImpl
createAssetManager
创建ApkAssets作为参数,使用AssetManager.Builder创建AssetManager

ApkAssets描述:大概意思是可以使用一个apk的path创建ApkAssets

   /**
     * Creates a new ApkAssets instance from the given path on disk.
     *
     * @param path The path to an APK on disk.
     * @return a new instance of ApkAssets.
     * @throws IOException if a disk I/O error or parsing error occurred.
     */
    public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
        return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/);
    }

AssetManager.Builder

public AssetManager build() {
        // Retrieving the system ApkAssets forces their creation as well.
        final ApkAssets[] systemApkAssets = getSystem().getApkAssets();

        final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
        final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];

        System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);

        final int userApkAssetCount = mUserApkAssets.size();
        for (int i = 0; i < userApkAssetCount; i++) {
            apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
        }

        // Calling this constructor prevents creation of system ApkAssets, which we took care
        // of in this Builder.
        final AssetManager assetManager = new AssetManager(false /*sentinel*/);
        assetManager.mApkAssets = apkAssets;
        AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
                false /*invalidateCaches*/);
        return assetManager;
    }
}

创建了AssetManager对象并调用nativeSetApkAssets设置了AssetManager的参数

到此为止,我们可以得到如下结论

1.app的res资源使用Resources来访问,最终都会交给AssetManager来执行资源的访问和返回

2.AssetManager的实例需要一个路径参数,那么我们可以通过创建自己的AssetManager,并设置一个目标apk路径,以此来实现加载目标apk的res资源

3.换肤思路

经过上面的源码分析,我们大概可以有如下的换肤思路:

1.activity加载View的时候使用Factory来截获View的加载过程,然后加载时记录Activity的每一个View需要调整的属性;

2.制作一个插件apk文件,将需要换肤的res属性定义到该apk中,原则是命名一致;

3.使用AssetManager加载插件apk的,就可以获取到插件apk的res属性,换肤的操作只需要将记录的View重新设置属性完成换肤

你可能感兴趣的:(Android LayoutInflater inflate过程(插件化换肤原理))