Android换肤引擎原理分析

1、LayoutInflater解析xml布局

1.1、解析流程

解析xml的源码流程大概如下:

//LayoutInflater.java(android-29)

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
	synchronized (mConstructorArgs) {
            ...
            // Temp is the root view that was found in the xml
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);   
        	...
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ...
        try {
            // 1、用Factory2尝试创建view
            View view = tryCreateView(parent, name, context, attrs);
			// 2、创建失败则反射创建
            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;
        }
    	...
}
// 用Factory2创建view
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;
        }
        ...
        return view;
}

解析时显尝试用Factory2去创建,失败后才用反射的方式,一个目的是为了减少反射的调用,同时只要在合适的时机设置自定义的Factory2,就能拦截到所有要创建的view了。

1.2、设置Factory2

无论是用Fragment还是直接用Activity展示UI,拿到的LayoutInflater都是Activity的,因此直接关注Activity的创建生命周期。

// AppCompatActivity.java
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
	...
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        ...
    }
// AppCompatDelegateImplV9.java   
public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            // 设置Factory2
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

AppCompatActivity执行onCreate()时会创建AppCompatDelegate代理,AppCompatDelegate是个接口,installViewFactory()方法的实现类只有AppCompatDelegateImplV9,通过源码可以看到在installViewFactory()里执行了设置Factory2。AppCompatDelegateImplV9的实现是把一些=View做替换,如“TextView”替换成"AppCompatTextView"。因此当继承AppCompatActivity时,只需在super.onCreate()之前设置自定义Factory2,就能实现view创建的拦截。

小结:

  1. 自定义Factory2拦截view,执行换肤
  2. 在Activity super.onCreate()之前setFactory2(),让自定义的Factory2生效

2、装载皮肤包

2.1、装载

Android利用Resource(7.0?之后实现放到ResourceImpl中)、AssetManager来管理资源的获取。Resource的创建需要AssetManager实例,AssetManager提供了装载额外apk资源的方法:

// Android低版本
	/**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

// 高版本
	/**
     * Changes the asset paths in this AssetManager. This replaces the {@link #addAssetPath(String)}
     * family of methods.
     *
     * @param apkAssets The new set of paths.
     * @param invalidateCaches Whether to invalidate any caches. This should almost always be true.
     *                         Set this to false if you are appending new resources
     *                         (not new configurations).
     * @hide
     */
    public void setApkAssets(@NonNull ApkAssets[] apkAssets, boolean invalidateCaches) {}

// ApkAssets的创建方式
	/**
     * Creates a new ApkAssets instance from the given path on disk.
     *
     * @param path The path to an APK on disk.
     * @param system When true, the APK is loaded as a system APK (framework).
     * @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, boolean system)
            throws IOException {
        return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/);
    }

所以需要创建AssetManager对象,再调用以上方法把皮肤包apk路径传入,一个皮肤包对应一个Resource。

2.2、获取皮肤包的资源

一般获取资源需要通过指定id,如R.drawable.xxxx,但在不同的Resource中,相同的资源id不一定相同,即原apk的资源id和皮肤包apk的资源id不一致,因此无法直接用原apk的资源id,需要先找到该资源在皮肤apk里对应的id。Resource提供了对应的方法:

// Resource.java
// 1.获取EntryName
public String getResourceEntryName(@AnyRes int resid) throws NotFoundException;
// 2.获取TypeName
public String getResourceTypeName(@AnyRes int resid) throws NotFoundException;
// 3.根据EntryName、TypeName获取id
public int getIdentifier(String name, String defType, String defPackage);

具体代码为:

// 1.用原apk的Resource得到对应id的EntryName、TypeName
String originalResEntryName = originalResources.getResourceEntryName(originalResId);
String originalResTypeName = originalResources.getResourceTypeName(originalResId);

// 2.用插件apk的Resource和EntryName、TypeName反过来得到id
skinResId = pluginResources.getIdentifier(originalResEntryName, originalResTypeName, mPacketName);

// 3.用得到的id获取资源
Drawable drawable = pluginResources.getDrawable(skinResId)

因为是用资源名来实现资源查找,因此原apk里的资源名必须和皮肤apk里的资源名一致,否则在皮肤包中会找不到相应的id。
小结:

  1. 创建AssetManager对象,皮肤包apk路径作为参数调用指定方法(addAssetPath()/setApkAssets())
  2. 创建Resource对象管理该皮肤包的资源
  3. 根据原id得到资源名;根据资源名得到皮肤包内对应的id;根据id得到相应的资源
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值