插件化换肤思路

了解我们当前activity的层级

在这里插入图片描述

上面这张图想必大家都已经有所了解,在这里我就不多说了。
查找activity加载这些代码的时候有一个点顺便说一下,我自己也是过一段时间就忘记了。
我们使用layoutinflater去实例化布局的时候,第三个参数总是传递false。

//实例化的时候会先创建一个temp的View
 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);
                        //传递false才会进去
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            //添加根View的布局参数
                            temp.setLayoutParams(params);
                        }
                    }

除了自定义View之外我们平时这种实例化xml的肯定都要啊,所以都传递false。

Activity的布局是怎么加载的

所有的View都是通过createView创建的,反射创建并缓存起来。

 clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.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);
                ...
                 try {
                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;
            } finally {
                mConstructorArgs[0] = lastContext;
            }

在创建之前有这么一行代码:

View view = tryCreateView(parent, name, context, attrs);
 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;
    }

mFactory为空的时候才会正常走流程,默认就是空的,mFactory2 是继承了mFactory2,这两个接口都是只有一个方法就是上面的createViewFromTag,他们的实现类都实现了它。

那么我们想要在实例化View的时候做一些事情有两种方式,一种直接替换了layoutInflater,一种直接塞进去一个mFactory,让它走我们的mFactory流程创建,而不走正常的创建流程。
我们自己的创建流程必要的代码直接复制系统正常的创建流程,在其中的关键点加入我们自己的代码即可。

编译后的APK资源

在这里插入图片描述
resource文件下面记录了res下面的资源文件的索引,ID、name、索引位置default
我们的平时也经常用resource的资源,这没什么好解释的。

加载资源

我们平时使用资源都是从context里获取resource,context的实现类是ContextImpl,直接找ContextImpl

public ContextImpl getSystemContext() {
        synchronized (this) {
            if (mSystemContext == null) {
                mSystemContext = ContextImpl.createSystemContext(this);
            }
            return mSystemContext;
        }
    }
 @UnsupportedAppUsage
    static ContextImpl createSystemContext(ActivityThread mainThread) {
        LoadedApk packageInfo = new LoadedApk(mainThread);
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, null,
                0, null, null);
         //关键代码,从包信息获取资源
        context.setResources(packageInfo.getResources());
        context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
                context.mResourcesManager.getDisplayMetrics());
        context.mIsSystemOrSystemUiContext = true;
        return context;
    }
   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");
            }
			//从资源管理器ResourcesManager的getResources方法返回资源resource
			//关键参数名mResDir
            mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader(), null);
        }
        return mResources;
    }
 public @Nullable Resources getResources(
 			//持有资源地址
            @Nullable String resDir,
			....
			//资源的实现类,30版本是放在前面,之前是放在return后面创建的
			//资源的创建肯定在这个类里面,后面不用跟了
			 ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
			 ...
			//通过createResources创建资源
         return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
    }
  private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
            @NonNull ResourcesKey key) {
        ResourcesImpl impl = findResourcesImplForKeyLocked(key);
        if (impl == null) {
        	//类似这种代码,第一次进来肯定是null的
            impl = createResourcesImpl(key);
            if (impl != null) {
                mResourceImpls.put(key, new WeakReference<>(impl));
            }
        }
        return impl;
    }
 private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
 		...
 		//关键代码,跟进去,impl持有AssetManager 
        final AssetManager assets = createAssetManager(key);
        ...
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        return impl;
    }
//30版本是建造者设计模式,以前是直接new,直接set。
//以前再跟进去是一个native方法现在直接提供java层的给开发者使用
 builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,

读取apk的资源文件,是一个private,不过我们可以向办法使用它

 private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
            throws IOException {
        final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
        ApkAssets apkAssets = null;
        if (mLoadedApkAssets != null) {
            apkAssets = mLoadedApkAssets.get(newKey);
            if (apkAssets != null && apkAssets.isUpToDate()) {
                return apkAssets;
            }
        }

        // Optimistically check if this ApkAssets exists somewhere else.
        final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(newKey);
        if (apkAssetsRef != null) {
            apkAssets = apkAssetsRef.get();
            if (apkAssets != null && apkAssets.isUpToDate()) {
                if (mLoadedApkAssets != null) {
                    mLoadedApkAssets.put(newKey, apkAssets);
                }

                return apkAssets;
            } else {
                // Clean up the reference.
                mCachedApkAssets.remove(newKey);
            }
        }

        // We must load this from disk.
        if (overlay) {
            apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path), 0 /*flags*/);
        } else {
            apkAssets = ApkAssets.loadFromPath(path, sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
        }

        if (mLoadedApkAssets != null) {
            mLoadedApkAssets.put(newKey, apkAssets);
        }

        mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
        return apkAssets;
    }

这个类的层次其实非常简单,我们平时的new Rosource().getXXXX,你可以点进去看下,调用ContextImpl,再跟进去又调用到ContextImpl的AssetManager。

换肤思路

1、利用LayoutInflater或者mFactory2创建View的过程收集XML信息
2、记录要换肤的属性,一个界面有多个View,一个View又有多个属性,设计一个对象持有它们。
3、读取准备好的皮肤包内容,需要先加载进来,使用反射。
4、执行换肤,将要替换的View的属性重新设置,资源从换肤APK里面的资源获取。

在View的创建过程中记录需要换肤的属性,例如下面这些:

mAttributes.add("background");
        mAttributes.add("src");
        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");

1、先创建一个对象,记录View的属性名字和资源ID,也就是上面那些。
2、它是被另外一个类持有,作为一个成员变量,另外一个变量记录着View对象。
3、再用一个数组持有当前界面所有的View。
4、换肤的时候顶部的statusBarColorResId和navigationBarColor也要换。

private static int[] STATUSBAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr
            .navigationBarColor
    };
```java
  public static int[] getResId(Context context, int[] attrs) {
        int[] resIds = new int[attrs.length];
        TypedArray a = context.obtainStyledAttributes(attrs);
        for (int i = 0; i < attrs.length; i++) {
            resIds[i] = a.getResourceId(i, 0);
        }
        a.recycle();
        return resIds;
    }

5.0以上才行

 //获得 statusBarColor 与 nanavigationBarColor (状态栏颜色)
        //当与 colorPrimaryDark  不同时 以statusBarColor为准
        int[] resIds = getResId(activity, STATUSBAR_COLOR_ATTRS);
        int statusBarColorResId = resIds[0];
        int navigationBarColor = resIds[1];

自定义View需要自己定义个接口,让所有的自定义View实现它,并设计一个方法,在方法里执行即可。

在Aplication监听所有的界面创建,在setContentView替换掉工厂factory,并收集好信息。

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //控件的收集在这里完成
        setContentView(R.layout.activity_main);

在setcontectView走完创建View的流程之后我们也就收集了所有的信息。

根据标志位是否换肤判断,之后我们可以对当前主APK里面的View的同名属性在插件APK加载的资源里面查找,如果能找到,则使用插件APK的资源,就达到了换肤目的。

虽然说设置View的属性这样会设置两次,但是性能还是非常好,无闪烁延迟等问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值