本篇文章是针对Android端换肤框架Android-Skin-Loader的源码解析

整个框架的架构

android skinmanager 换肤 android-skin-loader_ide

从加载皮肤说起
SkinManager.getInstance( ).load

一行代码便实现了换肤功能,那么进入该方法看看具体是怎样实现的

SkinManager.java
public void load(String skinPackagePath, final ILoaderListener callback) {
        
        new AsyncTask<String, Void, Resources>() {

            protected void onPreExecute() {
                if (callback != null) {
                    callback.onStart();  //对外通知 开始加载皮肤
                }
            };

            @Override
            protected Resources doInBackground(String... params) {
                try {
                    if (params.length == 1) {
                        String skinPkgPath = params[0];
                        
                        File file = new File(skinPkgPath); 
                        if(file == null || !file.exists()){
                            return null;
                        }
                        
                        PackageManager mPm = context.getPackageManager();
                        PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES); //获取apk包信息
                        skinPackageName = mInfo.packageName;
                        
                        //反射构造AssetManager实例,将皮肤包加载进内存,然后通过新构造的Resources去加载皮肤资源
                        AssetManager assetManager = AssetManager.class.newInstance();
                        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                        addAssetPath.invoke(assetManager, skinPkgPath);

                        Resources superRes = context.getResources();
                        Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
                        
                        SkinConfig.saveSkinPath(context, skinPkgPath); //保存皮肤包的绝对路径
                        
                        skinPath = skinPkgPath;
                        isDefaultSkin = false;
                        return skinResource;
                    }
                    return null;
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }
            };

            protected void onPostExecute(Resources result) {
                mResources = result; //得到异步处理结果,然后在SkinManager实例中维持Resources

                if (mResources != null) {
                    if (callback != null) callback.onSuccess(); //对外通知 加载皮肤成功
                    notifySkinUpdate(); //通知观察者改变皮肤
                }else{
                    isDefaultSkin = true;
                    if (callback != null) callback.onFailed(); //对外通知 加载皮肤失败
                }
            };

        }.execute(skinPackagePath);
    }

第一个参数代表外部皮肤包的绝对路径(通过gradle打包),第二个参数是加载皮肤成功与否的回调接口;
load方法主要构造了一个AsyncTask执行异步任务,通过PackageManager获取皮肤的包信息(皮肤本质上是个apk),利用反射构造AssetManager实例,调用addAssetPath方法将皮肤包加载进内存

addAssetPath方法中的关键代码

public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
 }
 
private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
        ......
        final ApkAssets assets;
            try {
                if (overlay) {
                    assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);
                } else {
                    assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);
                }
            } catch (IOException e) {
                return 0;
            }
            mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
            mApkAssets[count] = assets;
        ......
        }
    }

android源码中对ApkAssets的解释:The loaded, immutable, in-memory representation of an APK.
也就是说,是Apk包在内存中的类实例

接着load方法分析,利用新构造的AssetManager去new Resources目的是通过Resources来动态加载资源文件,执行完异步任务后,就在onPostExecute方法中将新的Resources赋值给字段mResources,这样加载皮肤时使用mResources,恢复默认皮肤时使用context.getResources( )加载资源,最后notifySkinUpdate( )通知view改变皮肤

在进入notifySkinUpdate( )分析之前,我们先分析一下SkinManager类

SkinManager

为了便于分析逻辑,将部分代码省略

public class SkinManager implements ISkinLoader{
    
    ......

    private List<ISkinUpdate> skinObservers;
    private String skinPackageName;
    private Resources mResources;
    private String skinPath;
    private boolean isDefaultSkin = false;
    
    ......

    public void restoreDefaultTheme(){
        ......
    }

    public void load(){
        ......
    }
    
    public void load(ILoaderListener callback){
        ......
    }
    
    public void load(String skinPackagePath, final ILoaderListener callback) {
        ......
    }
    
    @Override
    public void attach(ISkinUpdate observer) {
        if(skinObservers == null){
            skinObservers = new ArrayList<ISkinUpdate>();
        }
        if(!skinObservers.contains(observer)){
            skinObservers.add(observer);
        }
    }

    @Override
    public void detach(ISkinUpdate observer) {
        if(skinObservers == null) return;
        if(skinObservers.contains(observer)){
            skinObservers.remove(observer);
        }
    }

    @Override
    public void notifySkinUpdate() {
        if(skinObservers == null) return;
        for(ISkinUpdate observer : skinObservers){
            observer.onThemeUpdate();
        }
    }
    
    public int getColor(int resId){
        ......
    }
    
    
    public Drawable getDrawable(int resId){
        ......
    }
    
}

在Application中初始化SkinManager,类中skinObservers字段存储所有的观察者,这个观察者是你实现了ISkinUpdate接口的Activity或Fragment

public interface ISkinUpdate {
    void onThemeUpdate();   
}

注意该类中的attach与detach方法,分别将 观察者(activity和Fragment) 建立订阅或解除订阅关系
接下来我们再来分析下notifySkinUpdate( )方法,遍历所有的观察者,并调用观察者的onThemeUpdate方法完成皮肤切换,那么我们看看观察者(Activity或Fragment)的onThemeUpdate需要如何实现

@Override
public void onThemeUpdate() {
    if(!isResponseOnSkinChanging){
        return;
    }
    mSkinInflaterFactory.applySkin();
}

进入SkinInflaterFactory的applySkin( )看看

public void applySkin(){
    if(ListUtils.isEmpty(mSkinItems)){
        return;
    }
        
    for(SkinItem si : mSkinItems){
        if(si.view == null){
            continue;
        }
        si.apply();
    }
}

遍历list中的SkinItem,并调用apply( )方法,那么SkinItem又是什么东西呢?

public class SkinItem {
    
    public View view;
    
    public List<SkinAttr> attrs;
    
    public SkinItem(){
        attrs = new ArrayList<SkinAttr>();
    }
    
    public void apply(){
        if(ListUtils.isEmpty(attrs)){
            return;
        }
        for(SkinAttr at : attrs){
            at.apply(view);
        }
    }
    
    public void clean(){
        if(ListUtils.isEmpty(attrs)){
            return;
        }
        for(SkinAttr at : attrs){
            at = null;
        }
    }
}

SkinItem的代码十分简洁,我们看看它的成员变量,SkinItem中持有了需要换肤的View,以及更换资源的属性(封装在SkinAttr中),apply( )方法实际上的任务是遍历并调用SkinAttr.apply(view)

public abstract class SkinAttr {
    
    protected static final String RES_TYPE_NAME_COLOR = "color";
    protected static final String RES_TYPE_NAME_DRAWABLE = "drawable";
    
    /**
     * name of the attr, ex: background or textSize or textColor
     */
    public String attrName;
    
    /**
     * id of the attr value refered to, normally is [2130745655]
     */
    public int attrValueRefId;
    
    /**
     * entry name of the value , such as [app_exit_btn_background]
     */
    public String attrValueRefName;
    
    /**
     * type of the value , such as color or drawable
     */
    public String attrValueTypeName;
    
    /**
     * Use to apply view with new TypedValue
     * @param view
     * @param tv
     */
    public abstract void apply(View view);
}

SkinAttr中每个字段的含义就不赘述了,注释还是比较详尽;
下面我们分析具体的SkinAttr实现类,以框架中的BackgroundAttr类为例

public class BackgroundAttr extends SkinAttr {

    @Override
    public void apply(View view) {
        
        if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){ //当属性type为color
            view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
        }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){ //当属性type为drawable
            Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
            view.setBackground(bg);
        }
    }
}

为view的属性设置新的资源文件,我们进入kinManager.getInstance( ).getColor( )中看看具体实现

public int getColor(int resId){
    int originColor = context.getResources().getColor(resId);
    if(mResources == null || isDefaultSkin){
        return originColor;
    }
        
    String resName = context.getResources().getResourceEntryName(resId);
        
    int trueResId = mResources.getIdentifier(resName, "color", skinPackageName); //根据加载到内存中的皮肤包(apk)的packageName,得到皮肤包中的资源id
    int trueColor = 0;
        
    try{
        trueColor = mResources.getColor(trueResId);
    }catch(NotFoundException e){
        e.printStackTrace();
        trueColor = originColor;
    }
        
    return trueColor;
}

如果是默认皮肤,则使用context.getResources().getColor(resId)加载,如果是使用其他皮肤,则使用mResources.getColor(trueResId)加载,完成view属性的改变,即 完成皮肤切换

但你是否还有疑问,究竟什么时候确定哪些view是需要换肤的?什么时候将需要换肤的view封装进了SkinItem中,并使SkinInflaterFactory持有SkinItem?

请看下面的解析?

SkinInflaterFactory

为了便于分析逻辑,将部分代码省略

public class SkinInflaterFactory implements Factory {
    
    /**
     * Store the view item that need skin changing in the activity
     */
    private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
    
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        ......
    }
    
    
    private View createView(Context context, String name, AttributeSet attrs) {
        ......
    }

    
    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
        ......
    }
    
    public void applySkin(){
        ......
    }
    
    ......
    
    public void addSkinView(SkinItem item){
        mSkinItems.add(item);
    }
    
    public void clean(){
        ......
    }
}

SkinInflaterFactory实现了LayoutInflater的Factory接口,为什么要实现这个接口?

因为不自定义Factory,那么LayoutInflater就会交给系统去完成由xml布局到View实例的创建,自定义Factory后,在观察者(Activity或Fragment)中设置自定义的Factory

//Activity
getLayoutInflater().setFactory(mSkinInflaterFactory);
//Fragment
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
        field.setAccessible(true);
        field.setBoolean(getLayoutInflater(), false);
        
        mSkinInflaterFactory = new SkinInflaterFactory();
        getLayoutInflater().setFactory(mSkinInflaterFactory);

这样我们就代理了由xml布局创建View实例的过程,当由xml布局文件创建View实例时,会调用SkinInflaterFactory的onCreateView( )方法

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    // 判断xml布局中,需要换肤的控件是否将SkinConfig.NAMESPACE 命名空间下的SkinConfig.ATTR_SKIN_ENABLE属性设置为true
    boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
    if (!isSkinEnable){
            return null;
    }
        
    View view = createView(context, name, attrs); //由xml创建View实例
        
    if (view == null){
        return null;
    }
        
    parseSkinAttr(context, attrs, view);
        
    return view;
}

private View createView(Context context, String name, AttributeSet attrs) {
        View view = null;
        try {
            if (-1 == name.indexOf('.')){
                if ("View".equals(name)) {
                    view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
                } 
                if (view == null) {
                    view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
                } 
                if (view == null) {
                    view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
                } 
            }else {
                view = LayoutInflater.from(context).createView(name, null, attrs);
            }

            L.i("about to create " + name);

        } catch (Exception e) { 
            ......
        }
        return view;
    }

通过LayoutInflater的createView方法,根据xml标签中的控件名name和AttributeSet完成View实例的创建

LayoutInflater的createView关键源码如下

public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException {
    clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
    ......
    constructor = clazz.getConstructor(mConstructorSignature);
    ......
    args[1] = attrs;
    final View view = constructor.newInstance(args);
    ......
}

通过类加载和反射创建了View实例,创建View实例后,将调用parseSkinAttr将需要换肤的view和属性封装在SkinItem中

private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
    List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
        
    for (int i = 0; i < attrs.getAttributeCount(); i++){
        String attrName = attrs.getAttributeName(i);
        String attrValue = attrs.getAttributeValue(i);
            
        if(!AttrFactory.isSupportedAttr(attrName)){
            continue;
        }
            
        if(attrValue.startsWith("@")){
            try {
                int id = Integer.parseInt(attrValue.substring(1));
                String entryName = context.getResources().getResourceEntryName(id);
                String typeName = context.getResources().getResourceTypeName(id);
                SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); //封装需要切换资源的view属性
                if (mSkinAttr != null) {
                    viewAttrs.add(mSkinAttr);
                }
            } catch (NumberFormatException e) {
                e.printStackTrace();
            } catch (NotFoundException e) {
                e.printStackTrace();
            }
        }
    }
        
    if(!ListUtils.isEmpty(viewAttrs)){
        SkinItem skinItem = new SkinItem();
        skinItem.view = view;
        skinItem.attrs = viewAttrs;

        mSkinItems.add(skinItem); //在这里,将需要换肤的SkinItem(封装了View)添加到SkinInflaterFactory的list中
            
        if(SkinManager.getInstance().isExternalSkin()){
            skinItem.apply();
        }
    }
}

属性是如何封装的呢?

public class AttrFactory {
    
    public static final String BACKGROUND = "background";
    public static final String TEXT_COLOR = "textColor";
    public static final String LIST_SELECTOR = "listSelector";
    public static final String DIVIDER = "divider";
    
    public static SkinAttr get(String attrName, int attrValueRefId, String attrValueRefName, String typeName){
        
        SkinAttr mSkinAttr = null;
        
        if(BACKGROUND.equals(attrName)){ 
            mSkinAttr = new BackgroundAttr();
        }else if(TEXT_COLOR.equals(attrName)){ 
            mSkinAttr = new TextColorAttr();
        }else if(LIST_SELECTOR.equals(attrName)){ 
            mSkinAttr = new ListSelectorAttr();
        }else if(DIVIDER.equals(attrName)){ 
            mSkinAttr = new DividerAttr();
        }else{
            return null;
        }
        
        mSkinAttr.attrName = attrName;
        mSkinAttr.attrValueRefId = attrValueRefId;
        mSkinAttr.attrValueRefName = attrValueRefName;
        mSkinAttr.attrValueTypeName = typeName;
        return mSkinAttr;
    }
    
    /**
     * Check whether the attribute is supported
     */
    public static boolean isSupportedAttr(String attrName){
        ......
    }
}

将资源的属性名,id,资源名,资源类型封装在SkinAttr中

最后在SkinInflaterFactory中持有一个list,list包含需要换肤SkinItem,SkinItem封装了View和属性

总结:SkinManager持有观察者(Activity或Fragment),当切换皮肤时,利用Resources动态加载皮肤资源,加载成功后,对外通知切换皮肤,遍历观察者,每个观察者中持有SkinFactory,每个SkinFactory中持有需要换肤的View,也就是List,调用每个SkinAttr中的apply( )方法,完成view属性资源的切换