如何应对Android面试官->布局原理与xml解析,手写插件化换肤框架核心实现(上)

前言

本节内容分为上下两章,上章主要介绍布局流程和 xml 解析流程,下章带着大家手写插件化换肤框架的核心实现;

布局流程

想要布局,我们必须要启动 Activity 然后才能进行布局的渲染操作,启动 Activity 我们可以通过 ActivityThread 的 performLaunchActivity 看起,本章不详解 Activity 的启动流程,感兴趣的可以关注我,在 FrameWork 系列我后面会详细讲解,所以 我们进入 performLaunchActivity 方法看下:

typescript 复制代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 
    ...
    // 省略部分代码
    // 创建 Activity
    activity = mInstrumentation.newActivity(        
        cl, component.getClassName(), r.intent);    if (activity != null) {
        Window window = null;        if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {    
            window = r.mPendingRemoveWindow;
        }
        // 
        ...
        // 省略部分代码
        // 可以看到,attach 的时候,将 window 传递了过去,我们去 attach 看下
        activity.attach(appContext, this, getInstrumentation(), r.token,        
                r.ident, app, r.intent, r.activityInfo, title, r.parent,        
                r.embeddedID, r.lastNonConfigurationInstances, config,        
                r.referrer, r.voiceInteractor, window, r.configCallback,        
                r.assistToken);        
    }
}

进入 Activity 的 attach 方法看下:

arduino 复制代码
final void attach(Context context, ActivityThread aThread,        
        Instrumentation instr, IBinder token, int ident,        
        Application application, Intent intent, ActivityInfo info,        
        CharSequence title, Activity parent, String id,        
        NonConfigurationInstances lastNonConfigurationInstances,        
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,        
        Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
    // 
    ...
    // 省略部分代码        
    // window 在 attach 方法里进行了 初始化
    mWindow = new PhoneWindow(this, window, activityConfigCallback);

    //
    ...
    // 省略部分代码
}

也就是说 Activity 中 持有着一个 PhoneWindow,这里建立了 Activity 和 Window 的关联;

当 Activity 执行 onCreate 的时候,我们需要调用 setContentView 设置布局,这个setContentView 就是 PhoneWindow 提供的一个方法,我们进入这个方法看下:

scss 复制代码
public void setContentView(int layoutResID) {    
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window    
    // decor, when theme attributes and the like are crystalized. Do not check the feature    
    // before this happens.    
    if (mContentParent == null) {
        // 第一个知识点        
        installDecor();    
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {        
        mContentParent.removeAllViews();    
    }    
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {        
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,                
                    getContext());        
                    transitionTo(newScene);    
    } else {
        // 第二个知识点        
        mLayoutInflater.inflate(layoutResID, mContentParent);    
    }    
    mContentParent.requestApplyInsets();    
    final Callback cb = getCallback();    
    if (cb != null && !isDestroyed()) {        
        cb.onContentChanged();    
    }    
    mContentParentExplicitlySet = true;
}

setContentView 方法中主要关注两个方法的调用,installDecor() 和 mLayoutInflater.inflate();

首先我们来看下 installDecor 都干了什么?

installDecor

scss 复制代码
private void installDecor() {
    //
    ...
    // 省略部分代码
    if (mDecor == null) {    
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
    } else {
        mDecor.setWindow(this);
    }
    // 使用 DecorView 生成布局
    if (mContentParent == null) {    
        mContentParent = generateLayout(mDecor);
    }
    // 
    ...
    // 省略部分代码
}

如果 mDecor 为空,就调用 generateDecor 方法,进行创建,我们进入这个方法看一下:

generateDecor

scss 复制代码
protected DecorView generateDecor(int featureId) {    
    // System process doesn't have application context and in that case we need to directly use    
    // the context we have. Otherwise we want the application context, so we don't cling to the    
    // activity.    
    Context context;    
    if (mUseDecorContext) {        
        Context applicationContext = getContext().getApplicationContext();        
        if (applicationContext == null) {            
            context = getContext();        
        } else {            
            context = new DecorContext(applicationContext, this);            
            if (mTheme != -1) {                
                context.setTheme(mTheme);            
            }        
        }    
    } else {        
        context = getContext();    
    }    
    return new DecorView(context, featureId, this, getAttributes());}

最终就是 new 了一个 DecorView,使用这个 DecorView 来设置主体和属性,也就是 generateLayout 方法;

generateLayout

scss 复制代码
protected ViewGroup generateLayout(DecorView decor) {
    // 省略部分代码
    ...
    // 
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);    
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
}

这个 mLayoutInflater 就是我们的布局解析器, layoutResource 就是系统的布局文件Id;

我们进入这个 onResourceLoaded 方法看一下

onResourceLoaded

java 复制代码
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    //
    ...
    // 省略部分代码
    final View root = inflater.inflate(layoutResource, null);    if (mDecorCaptionView != null) {    
        if (mDecorCaptionView.getParent() == null) {        
            addView(mDecorCaptionView,                
                new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));    
        }    
        mDecorCaptionView.addView(root,            
            new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {    
            // Put it below the color views.    
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
    }
    //
    ...
    // 省略部分代码
}

infalte,解析完系统的 xml 之后,将 xml 中的 view 添加到 DecorView(FrameLayout)中

可以看到最终都是调用了 addView 方法,将系统的 xxx.xml 添加到视图;

DecorView中 嵌套着一个 contentParent(也是一个FragmeLayout)和一个 ViewStub,这个contentParent 就是 rootView;

我们接着回到 setContentView 中,看第二个知识点

mLayoutInflater.infalte

ini 复制代码
mLayoutInflater.inflate(layoutResID, mContentParent);

layoutResID,就是我们自己声明的 xml;

mContentParent,就是前面介绍的 generateLayout 方法中返回的对象;

scss 复制代码
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, 
            boolean attachToRoot) {
    // 
    ...
    // 省略部分代码
    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);    
        }
        rInflateChildren(parser, temp, attrs, true);        if (root != null && attachToRoot) {    
            root.addView(temp, params);
        }
        if (root == null || !attachToRoot) {    
            result = temp;
        }
    }
}

attachToRoot 这个参数用来决定是通过 xml 的形式设置根布局的布局参数,还是通过 addView 的方式设置根布局的布局参数;

如果 attachToRoot 为 fasle,则执行 temp.setLayoutParams,如果为 true 则不执行,那么就需要通过 addView 的方式给它设置布局参数;

本质上就是说:true 是动态添加的,false 非动态添加的;

root == null 构建的 view 将是一个独立的个体,属性无效;

root != null && attachToRoot == false

  1. 属性值会依托于 root 构建,所以此时的 xml 根布局的属性有效;
  2. 根布局产生的 view 不是 root 的子布局;

root != null && attachToRoot == true

  1. 属性值会依托于root构建,所以此时的xml根布局的属性有效;

  2. 根布局产生的view是root的子布局,通过addView实现;

    View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);

为什么是三个参数,当我们自定义 LayoutInflater 的时候 如果第三个参数传递 true 的时候 有时候会抛出异常,这是因为系统在设置 ViewSystem 的时候,期望所有的 View 都能够以树型结构来摆放,树型结构的特点就是每一个节点 都只有一个父亲,所以当第三个参数传递 true 的时候,就会进入到下面的代码逻辑中,也就是说传递进去的 ViewGroup root 是不能有父亲的,否则就会抛出异常,也就是说 addVIew 只能执行一次,这样就能保证每个 View 都只有自己的一个父亲;

csharp 复制代码
if (root != null && attachToRoot) {
   root.addView(temp, params);
}

addViewInner(child, index, params, false);

if (child.getParent() != null) {
            throw new IllegalStateException("The specified child already has a parent. " +
                    "You must call removeView() on the child's parent first.");
}

这就是第三个参数为什么传递 false 的原因,以及传递 true 的时候有时候会报错的原因;

我们接下来进入 createViewFromTag 方法看一下:

createViewFromTag

ini 复制代码
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;
    }    //
    ...
    // 省略部分代码
}

onCreatView 最终也会走到 createView 方法,所以我们进入 createView 方法看一下 View 是怎么创建的;

createView

less 复制代码
public final View createView(@NonNull Context viewContext, @NonNull String name,        
    @Nullable String prefix, @Nullable AttributeSet attrs)throws ClassNotFoundException, InflateException {
    if (constructor == null) {
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,        
                    mContext.getClassLoader()).asSubclass(View.class);        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    } else {
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,        
                    mContext.getClassLoader()).asSubclass(View.class);
    }
    // 
    ...
    // 省略部分代码
    final View view = constructor.newInstance(args);
    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;
}

在这里使用反射调用 2 个参数的构造方法实例化 view 对象;

所以经过 installDecor 和 mLayoutInflater.inflate 之后,我们的界面就是下面这张图上的样式

同时,我们插件化换肤的第一个点也就找到了,如果我们能替换 createView 方法为自己的 createView,那么在每一个 Activity 页面创建或者 Fragment 创建的时候,就能自己定义 View 的创建流程,并给 View 设置想要的颜色或者图片,那么怎么接管这个 createView 呢?我们继续看源码;

在 createView 之前,我们发现有一个 tryCreateView 方法,如果这个方法返回的 view 不为空的话,就会直接 return 这个 view,我们进去看一下;

tryCreateview

less 复制代码
public final View tryCreateView(@Nullable View parent, @NonNull String name,    
        @NonNull Context context,    
        @NonNull AttributeSet 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;
}

代码中写到 mFactory 和 mFactory2,我们看看这两个 Factory 的实现;

less 复制代码
public interface Factory {
    View onCreateView(@NonNull String name, @NonNull Context context,        
        @NonNull AttributeSet attrs);
}

public interface Factory2 extends Factory {
    View onCreateView(@Nullable View parent, @NonNull String name,        
        @NonNull Context context, @NonNull AttributeSet attrs);
}

两个接口,第二个点也就找到了,我们实现这其中的一个接口,通常都是实现 Factory2,监听每个 Activity 或者 Fragment 的 onCreate 方法,在这个方法中反射拿到 LayoutInflater,设置自己的 Factory2,就可以拦截 View 的创建过程,在 View 的创建的过程中给 View 设置从网上获取到的皮肤包颜色值;

接下来我们就要考虑皮肤包怎么加载?插件的形式,就肯定是后下载的方式,下载下来之后如何加载到内存呢?

首先我们来看下我们的 apk 结构

我们编译出来的 apk 下是有一个 resources.arsc 文件,这个文件记录了 res 下的资源的映射关系,当我们的 apk 安装之后,就会有一个单独的类来加载这些内容,那么这些资源的加载流程是怎样的呢?我们进入 ActivityThread 的 handleBindApplication 中看下:

typescript 复制代码
private void handleBindApplication(AppBindData data) {
    //
    ...
    // 省略部分代码
    Application app;    try {
        app = data.info.makeApplication(data.restrictedBackupMode, null);
    } catch(Exception e) {
        
    }
}

public Application makeApplication(boolean forceDefaultAppClass,        
        Instrumentation instrumentation) {
    //
    ...
    // 省略部分代码
    // 首先会创建一个上下文
    ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
}

我们进入这个 createAppContext 方法看下:

javascript 复制代码
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, null,            
                  0, null, opPackageName);    
    context.setResources(packageInfo.getResources());    
    context.mIsSystemOrSystemUiContext = isSystemOrSystemUI(context);    
    return context;
}

这个方法中,有一行比较关键的代码

ini 复制代码
context.setResources(packageInfo.getResources());

packageInfo.getResources(); 从 apk 中获取资源,然后设置给上下文;那么这个 getResources 中是怎么获取的呢?我们进入这个方法看一下:

typescript 复制代码
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(), null);    
    }    
    return mResources;
}

我们进入 ResourcesManager 的 getResources 方法看下:

less 复制代码
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,        
        @Nullable List<ResourcesLoader> loaders) {    
    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,                
                loaders == null ? null : loaders.toArray(new ResourcesLoader[0]));        
                classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();        
        if (activityToken != null) {            
            rebaseKeyForActivity(activityToken, key);        
        }        
        return createResources(activityToken, key, classLoader);    
    } finally {        
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);    
    }
}

这里最终调用了 createResources 方法进行 Resources 的创建,我们进入这个方法看下:

less 复制代码
private @Nullable Resources createResources(@Nullable IBinder activityToken,        
        @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    //
    ...
    // 省略部分代码
    ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
}

我们进入这个 findOrCreateResourcesImplForKeyLocked 方法看下:

ini 复制代码
private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(        
    @NonNull ResourcesKey key) {    
    ResourcesImpl impl = findResourcesImplForKeyLocked(key);    
    if (impl == null) {        
        impl = createResourcesImpl(key);        
        if (impl != null) {            
            mResourceImpls.put(key, new WeakReference<>(impl));        
        }    
    }    
    return impl;
}

这里进行了 ResourcesImpl 的创建,我们进入这个方法看下:

java 复制代码
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {    
    final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);    
    daj.setCompatibilityInfo(key.mCompatInfo);    
    final AssetManager assets = createAssetManager(key);    
    if (assets == null) {        
        return null;    
    }    
    final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);    
    final Configuration config = generateConfig(key, dm);    
    final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);    
    if (DEBUG) {        
        Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);    
    }    
    return impl;
}

这里创建了 AssetManager 和 Configuration 以及 ResourcesImpl;

createAssetManager 方法中有一个比较关键的方法:

arduino 复制代码
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,        
    false /*overlay*/));

这个方法会调用到 addAssetPathInternal 然后调度到的是 native 层,用来读取 apk 中的 resources.arsc 文件;所以最终的这个 arsc 文件的加载,就是通过 AssetManager 来实现的;

Resources 和 ResourcesImpl 以及 AssetManager 的关系如下:

包括我们在代码中获取资源信息的时候,最终也是调度到 AssetManager;我们可以随便找一个,通过 getResource().getDrawable(); 点进去看下:

less 复制代码
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {    
    final Drawable d = getDrawable(id, null);   
    return d;
}

public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)        
        throws NotFoundException {    
    return getDrawableForDensity(id, 0, theme);
}

public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {    
    final TypedValue value = obtainTempTypedValue();    
    try {        
        final ResourcesImpl impl = mResourcesImpl;        
        impl.getValueForDensity(id, density, value, true);        
        return loadDrawable(value, id, density, theme);    
    } finally {        
        releaseTempTypedValue(value);    
    }
}

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

最终调用到的就是 mAssets 的 getresourceValue 方法;

而插件化换肤的原理,就是将插件同名资源值进行替换;

arduino 复制代码
/** 
  * 1. 通过原始 app 中的 resId(R.id.XX) 获取到自己的名字 
  * 2. 根据名字和类型获取皮肤包中的 ID 
  * 
  * @param resId resId 
  */
private int getIdentifier(int resId) {    
    if (isDefaultSkin) {        
        return resId;    
    }    
    //    
    String resName = mAppResources.getResourceEntryName(resId);    
    String resType = mAppResources.getResourceTypeName(resId);    
    return mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
}

通过宿主 App 中的控件 id(例如 ImageView),也就是 R.id.test 获取这个控件在 resources.arsc 中对应的 resourceName 和 resourceType;

scss 复制代码
/** 
 * 获取颜色值 
 * 
 * @param resId resId 
 */
public int getColor(int resId) {    
    if (isDefaultSkin) {        
        return mAppResources.getColor(resId);    
    }    
    int skinId = getIdentifier(resId);    
    if (skinId == 0) {        
        return mAppResources.getColor(resId);    
    }    
    return mSkinResources.getColor(resId);
}

换肤的具体思路

  1. 收集XML数据;

  2. 利用 View 生产对象的过程中的 Factory2 接口;

  3. 统计需要换肤的属性;

  4. SkinAttribute 记录需要的属性,Factory2 中生产 view 的时候;

  5. public void applySkin() 修改属性(属性的值应该来自于皮肤包);

  6. 制作皮肤包;

  7. 生成一个无代码的APK文件即可;

  8. 读取皮肤包皮肤;

  9. SkinManager中 public void loadSkin(String skinPath) 读取资源包信息;

  10. 执行换肤;

  11. SkinManager 通知 SkinLayoutInflaterFactory更新 UI;

  12. 打开过的页面如何进行换肤?

  13. 打开过的页面 会被添加进 Observable 集合中,当执行 notifyObservers 的时候,会回调 update 方法,在这个方法中进行更新;

  14. 未打开过的页面如何进行换肤?

  15. SkinManager 在 init 的时候,会 hook Resources,将其默认的颜色值替换为皮肤插件包中的值,未打开过的 Activity 在执行 setContentVIew 的时候 就会使用皮肤包中的颜色值进行渲染;

这种插件化换肤的方案的优点:

  1. 用户体验,无闪烁换肤架构;
  2. 扩展和维护方便,入侵性小,低耦合;
  3. 插件化开发,任何APP都是你的皮肤包;
  4. 立即生效,无需要重启APP;

下一章预告

带着大家手写插件化换肤框架核心实现(下)

欢迎三连

来都来了,点个关注点个赞吧,你的支持是我最大的动力~~

相关推荐
hummhumm几秒前
Oracle 第29章:Oracle数据库未来展望
java·开发语言·数据库·python·sql·oracle·database
wainyz10 分钟前
Java NIO操作
java·开发语言·nio
工业3D_大熊15 分钟前
【虚拟仿真】CEETRON SDK在船舶流体与结构仿真中的应用解读
java·python·科技·信息可视化·c#·制造·虚拟现实
lzb_kkk24 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
CYRUS STUDIO36 分钟前
ARM64汇编寻址、汇编指令、指令编码方式
android·汇编·arm开发·arm·arm64
爬山算法1 小时前
Maven(28)如何使用Maven进行依赖解析?
java·maven
2401_857439691 小时前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索1 小时前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
weixin_449310841 小时前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql