Andorid 换肤框架AndSkin源码解析及优缺点

Posted 一口仨馍

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Andorid 换肤框架AndSkin源码解析及优缺点相关的知识,希望对你有一定的参考价值。

AndSkin简介及使用教程

AndSkin gaybug: https://github.com/RrtoyewxXu/andSkin

AndSkin 作者写的说明: http://blog.csdn.net/zhi184816/article/details/53436761

AndSkin源码解析

初始化

BaseSkinApplication解析

BaseSkinApplication中其实就一行初始化的代码:SkinLoader.getDefault().init(this)。其中SkinLoader.getDefault()采用单例返回了SkinLoader对象,中间啥也没干,故省去这部分代码。

    public void init(Context context) 
        mLoadSkinDeliver = new LoadSkinDeliver();
        DataManager.getDefault().init(context, mLoadSkinDeliver);

        String pluginAPKPackageName = DataManager.getDefault().getPluginPackageName();
        String pluginAPKPath = DataManager.getDefault().getPluginPath();
        String pluginAPKSuffix = DataManager.getDefault().getResourceSuffix();

        GlobalManager.getDefault().init(context, pluginAPKPackageName, pluginAPKPath, pluginAPKSuffix);
        ResourceManager.getDefault().init(pluginAPKPackageName, pluginAPKPath, pluginAPKSuffix, mLoadSkinDeliver);
    

初始化的代码基本都在这里。后续会用到这里初始化的很多对象,所以下面会比较详细的讲解这个方法及中间生成的各种对象。

LoadSkinDeliver是SkinLoader的内部类,继承自IDeliver,而且内部有个获取了主线程looper的Handler,功能是负责消息的分发。在后续的换肤操作消息分发都由LoadSkinDeliver负责通知到各个界面的各个View。

    private class LoadSkinDeliver implements IDeliver 
        private Handler mHandler = new Handler(Looper.getMainLooper());
        ...
    

DataManager实现了ILoadSkin接口,实际上这个类的作用只是采用SP的方式保存下当前皮肤的后缀(suffix),对于动态换肤还会保存插件包包名(plugin_package_name)和插件包路径(plugin_path)。作用相当于SP的封装类,相信读者能够看快看透。这里就不详细讲述了。

GlobalManager这个类就是个JavaBean,在内存中保存了ApplicationContext、PackageName、PluginAPKPackageName、PluginAPKPath和ResourceSuffix属性。除此之外,没有任何作用。

ResourceManager比较重要,也是动态换肤的精髓所在。ResourceManager实现了ILoadSkin接口,并持有一个Resource的引用。

public class ResourceManager implements ILoadSkin 
    private Resource mResource;
    private IDeliver mIDeliver;
    ...
    void init(String pluginPackageName, String pluginPath, String pluginSuffix, IDeliver deliver) 
        mIDeliver = deliver;
        smartCreateResource(pluginPackageName, pluginPath, pluginSuffix, true);
    

    private boolean smartCreateResource(String pluginPackageName, String pluginPath, String suffix, boolean firstInit) 
        boolean shouldCreate = checkIfReCreateDateResource(pluginPackageName, pluginPath, suffix);
        if (shouldCreate) 
            try 
                createDataResource(pluginPackageName, pluginPath, suffix);
                mIDeliver.postResourceManagerLoadSuccess(firstInit, pluginPackageName, pluginPath, suffix);
             catch (Exception e) 
                e.printStackTrace();
                mIDeliver.postResourceManagerLoadError(firstInit);
            
         else 
            mResource.changeResourceSuffix(suffix);
            mIDeliver.postResourceManagerLoadSuccess(false, pluginPackageName, pluginPath, suffix);
        

        return mResource != null;
    

smartCreateResource方法中首先会依据pluginPackageName, pluginPath, suffix判断是否需要重新生成Resource对象,这里依据是本地换肤还是动态换肤生成相应的LocalResource或者PluginResource对象。这里只是初始化,暂时不用深入,后续换肤章节中会详细解析这部分。目前是第一次实例化,mResource==null,即shouldCreate为true。

    private void createDataResource(String pluginPackageName, String pluginPath, String suffix) throws Exception 
        mResource = ResourceFactory.newInstance().createResource(pluginPackageName, pluginPath, suffix, mIDeliver);
    
public abstract class ResourceFactory 
    private ResourceFactory() 
    

    public static ResourceFactory newInstance() 
        return new ResourceFactoryImp();
    

    public abstract Resource createResource(String pluginPackageName, String pluginPath, String suffix, IDeliver deliver) throws Exception;


    static class ResourceFactoryImp extends ResourceFactory 
        private ResourceFactoryImp() 

        

        @Override
        public Resource createResource(String pluginPackageName, String pluginPath, String suffix, IDeliver deliver) throws Exception 
            String packageName = GlobalManager.getDefault().getPackageName();
            Context context = GlobalManager.getDefault().getApplicationContext();

            if (!TextUtils.isEmpty(pluginPackageName) && !pluginPackageName.equals(packageName)) 
                return new PluginResource(context, pluginPackageName, pluginPath, suffix);
            
            return new LocalResource(context, pluginPackageName, pluginPath, suffix);
        
    

mResource属性会在这里初始化,生成的逻辑也很简单,就是依据报名判断是不是动态换肤。如果不是,返回PluginResource,反之则返回LocalResource。这两个类都继承自Resource,并重写了部分方法,差别是PluginResource实例化时传入了动态插件(.apk)的AssetManager,这个AssetManager使用动态插件的路径构建的。这部分仍在会在换肤章节重点解析,现在回到ResourceManager#smartCreateResource。在获取到Resource之后,调用mIDeliver.postResourceManagerLoadSuccess分发消息到监听器中,告诉监听器初自己始化完毕。代码体现如下:

            mHandler.post(new Runnable() 
                @Override
                public void run() 
                    if (firstInit && mOnInitLoadSkinResourceListener != null) 
                        mOnInitLoadSkinResourceListener.onInitResourceSuccess();
                     else 
                        boolean findResourceSuccess = notifyAllChangeSkinObserverListToFindResource();

                        if (findResourceSuccess) 
                            postGetAllResourceSuccessOnMainThread(pluginPackageName, pluginPath, resourceSuffix);
                         else 
                            postGetResourceErrorOnMainThread();
                        
                    
                
            );

换肤

换肤需要换肤的Activity继承自BaseSkinActivity。BaseSkinActivity继承自AppCompatActivity,并实现了IChangeSkin接口。核心代码如下:

public class BaseSkinActivity extends AppCompatActivity implements IChangeSkin 
    protected BaseSkinActivity mActivity;
    private SkinLayoutInflater mSkinLayoutInflater;
    ...

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) 
        mActivity = this;
        if (shouldRegister()) 
            mSkinLayoutInflater = new SkinLayoutInflater(this);
        
        super.onCreate(savedInstanceState);
    

    @Override
    public void setContentView(@LayoutRes int layoutResID) 
        super.setContentView(layoutResID);
        if (shouldRegister()) 
            findLayoutInflaterSkinViews();
            generateStatusBarIfShould();
            SkinLoader.getDefault().register(mActivity);
        
    

由于LayoutInflaterCompat.setFactory只有在第一次调用的时候有效,所以AppCompatActivity#installViewFactory(在onCreate被调用,方法内会调用LayoutInflaterCompat.setFactory)在被调用之前提前调用LayoutInflaterCompat.setFactory。代码体现为:

public class SkinLayoutInflater 
    ...
    public SkinLayoutInflater(BaseSkinActivity baseSkinActivity) 
        this.mBaseSkinActivity = baseSkinActivity;
        mSkinInflaterFactory = new SkinInflaterFactory();

        mDynamicAddSkinViewList = new ArrayList<>();
        mLayoutInflaterSkinViewList = new ArrayList<>();

        LayoutInflaterCompat.setFactory(mBaseSkinActivity.getLayoutInflater(), mSkinInflaterFactory);
    
    ...

这里的SkinInflaterFactory实现了LayoutInflaterFactory接口,如此一来,在继承BaseSkinActivity的页面中,View从XML到变成View的解析工作就交给了SkinInflaterFactory。

下面回到BaseSkinActivity#setContentView。在调用super.setContentView(layoutResID)之后会调用findLayoutInflaterSkinViews方法。而在View从XML变成View之前,会调用SkinInflaterFactory#onCreateView。这部分逻辑体现在framework层的LayoutInflater#createViewFromTag。感兴趣的可以查看我的另一篇博文 Android XML布局文件解析过程源码解析

SkinInflaterFactory中有个属性mSkinViewList,其中保存了所有需要换肤View的id。接下来会分析怎么获取到的这些id,这也是换肤框架的一个核心难点所在。

public class SkinInflaterFactory implements LayoutInflaterFactory 
    private List<SkinView> mSkinViewList = new ArrayList<>();


    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) 
        boolean isSkinEnable = attrs.getAttributeBooleanValue(ConfigConstants.SKIN_NAMES_SPACE, ConfigConstants.ATTR_SKIN_ENABLE, false);
        String attrList = attrs.getAttributeValue(ConfigConstants.SKIN_NAMES_SPACE, ConfigConstants.ATTR_SKIN_LIST);
        if (isSkinEnable) 
            try 
                if (TextUtils.isEmpty(attrList)) 
                    parseSkinAttr(context, attrs, name);
                 else 
                    attrList = attrList.trim();
                    parseSkinAttrByAttrList(context, attrs, attrList, name);
                
             catch (Exception e) 
                e.printStackTrace();
                SkinL.e("解析xml文件失败,请检查xml文件");
            
        
        return null;
    
    ...

先看下大概的逻辑:

SKIN_NAMES_SPACE是常量:http://schemas.android.com/android/andSkin
ATTR_SKIN_ENABLE是常量:enable
ATTR_SKIN_LIST是常量:attrs

首先查看View是否设置了enable属性。如果没有,则不需要换肤。如果有,再去判断哪些属性需要换肤。这里能够获取View所有的属性及对应的值,所以查找及添加到mSkinViewList的代码就不一一解析了。跟普通写个list.add()没啥区别。需要注意的地方是,这里onCreateView返回了null,也就是说创建View的工作交给了LayoutInflater#onCreateView。还一个需要注意的是,这里保存的是View的id,并不是真正的View的引用。很明显,返回null的话,View这时候还没创建。

所以初始化及准备工作到这里就算真正完成了,接下来是真正调用换肤API实现换肤的解析。

本地换肤

假设我现在有两套皮肤,一套叫day,另一套叫night。day皮肤为默认皮肤,文件命名大概为:icon_search.png。night为夜间皮肤,文件命名大概为icon_search_night.png。那么,应用内由day皮肤切换到night的代码为:

SkinLoader.getDefault().loadSkin("night");

一行代码实现换肤,使用非常简单。跟进。

public class SkinLoader implements ILoadSkin 
    ...
    @Override
    public void loadSkin(String suffix) 
        loadSkin("", "", suffix);
    

    @Override
    public void loadSkin(String pluginPackageName, String pluginPath, String suffix) 
        loadSkinInner(pluginPackageName, pluginPath, suffix, true);
    

    private void loadSkinInner(String pluginPackageName, String pluginPath, String suffix, boolean needCallSkinChangeListener) 
        cancelLoadSkinTask();
        startLoadSkinTask(pluginPackageName, pluginPath, suffix, needCallSkinChangeListener);
    

    private void startLoadSkinTask(String pluginAPKPackageName, String pluginAPKPath, String resourceSuffix, boolean needCallSkinChangeListener) 
        mLoadSkinTask = new LoadSkinTask();
        mLoadSkinTask.setNeedCallSkinChangeListener(needCallSkinChangeListener);
        mLoadSkinTask.execute(pluginAPKPackageName, pluginAPKPath, resourceSuffix);
    
    ...

经过一些列的重载进入到startLoadSkinTask方法中。LoadSkinTask继承自AsyncTask,实例化mLoadSkinTask属性之后,设置needCallSkinChangeListener为true。最后调用execute()方法,并将pluginAPKPackageName、pluginAPKPath,、resourceSuffix传递进去。因为是本地换肤,pluginAPKPackageName和pluginAPKPath参数暂时为null。

在onPreExecute()中会通知所有的观察者换肤开始,通常观察者只有一个。在doInBackground中调用DataManager#loadSkin。跟进。

public class DataManager implements ILoadSkin 
    ...
    @Override
    public void loadSkin(String pluginPackageName, String pluginPath, String suffix) 
        if (pluginPackageName != null && pluginPackageName.equals(getPluginPackageName())
                && pluginPath != null && pluginPath.equals(getPluginPath())
                && suffix != null && suffix.equals(getResourceSuffix())) 

            mDeliver.postDataManagerLoadError();
         else 
            savePluginPackageName(pluginPackageName);
            savePluginPath(pluginPath);
            saveResourceSuffix(suffix);
            mDeliver.postDataManagerLoadSuccess(pluginPackageName, pluginPath, suffix);
        
    

else中三个save方法都是在SP文件中保存即将要应用的皮肤的信息,主要是suffix。

        @Override
        public void postDataManagerLoadSuccess(String pluginPackageName, String pluginPath, String resourceSuffix) 
            SkinL.d("保存本次换肤的相关信息成功");
            ResourceManager.getDefault().loadSkin(pluginPackageName, pluginPath, resourceSuffix);
        

经过在DataManager中保存SP信息之后,由LoadSkinDeliver分发消息到ResourceManager#loadSkin。

    @Override
    public void loadSkin(String pluginPackageName, String pluginPath, String suffix) 
        try 
            smartCreateResource(pluginPackageName, pluginPath, suffix, false);
         catch (Exception e) 
            e.printStackTrace();
            mIDeliver.postResourceManagerLoadError(false);
        
    

初始化的时候简单分析过smartCreateResource方法,和上次不同的是,这次最后一个参数firstInit为false。

    private boolean smartCreateResource(String pluginPackageName, String pluginPath, String suffix, boolean firstInit) 
        boolean shouldCreate = checkIfReCreateDateResource(pluginPackageName, pluginPath, suffix);
        SkinL.d("should create resource : " + shouldCreate);
        if (shouldCreate) 
            try 
                createDataResource(pluginPackageName, pluginPath, suffix);
                mIDeliver.postResourceManagerLoadSuccess(firstInit, pluginPackageName, pluginPath, suffix);
             catch (Exception e) 
                e.printStackTrace();
                mIDeliver.postResourceManagerLoadError(firstInit);
            
         else 
            mResource.changeResourceSuffix(suffix);
            mIDeliver.postResourceManagerLoadSuccess(false, pluginPackageName, pluginPath, suffix);
        

        return mResource != null;
    

由于初始化的时候,默认创建了LocalResource,所以这里shouldCreate为false,走else的逻辑。mResource.changeResourceSuffix(suffix)只是简单的记录night的后缀。之后继续由LoadSkinDeliver分发消息。

        public void postResourceManagerLoadSuccess(final boolean firstInit, final String pluginPackageName, final String pluginPath, final String resourceSuffix) 
            SkinL.d("生成Resource对象成功");

            mHandler.post(new Runnable() 
                @Override
                public void run() 
                    if (firstInit && mOnInitLoadSkinResourceListener != null) 
                        mOnInitLoadSkinResourceListener.onInitResourceSuccess();
                     else 
                        boolean findResourceSuccess = notifyAllChangeSkinObserverListToFindResource();

                        if (findResourceSuccess) 
                            postGetAllResourceSuccessOnMainThread(pluginPackageName, pluginPath, resourceSuffix);
                         else 
                            postGetResourceErrorOnMainThread();
                        
                    
                
            );
        

这次firstInit为false。终于要走换肤的逻辑了。。。

    private boolean notifyAllChangeSkinObserverListToFindResource() 
        boolean findResourceSuccess = true;
        SkinL.d("通知所有的观察者查找资源");
        for (IChangeSkin changeSkin : mChangeSkinObserverList) 
            findResourceSuccess = changeSkin.findResource();
            if (!findResourceSuccess) 
                break;
            
        
        return findResourceSuccess;
    

        @Override
        public void postGetAllResourceSuccessOnMainThread(String pluginPackageName, String pluginPath, String resourceSuffix) 
            SkinL.d("查找所有资源成功");
            GlobalManager.getDefault().flushPluginInfos(pluginPackageName, pluginPath, resourceSuffix);
            notifyAllChangeSkinObserverListToApplySKin();
        

    private void notifyAllChangeSkinObserverListToApplySKin() 
        SkinL.d("通知所有的组件进行换肤");
        for (IChangeSkin changeSkin : mChangeSkinObserverList) 
            changeSkin.changeSkin();
        
    

所有的BaseSkinActivity对象都会被add到mChangeSkinObserverList属性。也就是说首先会调用所有BaseSkinActivity对象的findResource方法,找到所有换肤需要的资源,之后再统一调用changeSkin。逻辑缕清了,直接顺序查看这两个方法。

public class BaseSkinActivity extends AppCompatActivity implements IChangeSkin 
    @Override
    public boolean findResource() 

        ...
        List<SkinView> layoutInflaterSkinViewList = mSkinLayoutInflater.getLayoutInflaterSkinViewList();
        for (IChangeSkin skinView : layoutInflaterSkinViewList) 
            findResourceSuccess = skinView.findResource();
            if (!findResourceSuccess) 
                break;
            
        
        ...
        return findResourceSuccess;
    

    @Override
    public void changeSkin() 
        List<SkinView> layoutInflaterSkinViewList = mSkinLayoutInflater.getLayoutInflaterSkinViewList();
        for (IChangeSkin skinView : layoutInflaterSkinViewList) 
            skinView.changeSkin();
        
    

SkinView实现了IChangeSkin接口,在执行SkinInflaterFactory#onCreateView时,添加进mSkinViewList的View,作为需要换肤View的包装类,其中保存了需要换肤View的id。在调用mSkinLayoutInflater.getLayoutInflaterSkinViewList()时,会将进行findViewById将id对应的View也存进SkinView。跟进。

public class SkinView implements IChangeSkin 
    @Override
    public boolean findResource() 
        boolean changed = true;
        for (BaseSkinAttr attr : mSkinAttrList) 
            changed = attr.findResource();
            if (!changed) 
                break;
            
        

        return changed;
    

    @Override
    public void changeSkin() 
        for (BaseSkinAttr attr : mSkinAttrList) 
            attr.applySkin(mView);
        
    

mSkinAttrList中保存了View所有需要换肤的属性。目前AndSkin只支持三种属性,分别是“background”、”src”和”TextColor”。BaseSkinAttr是个抽象类,三个子类分别为:BackgroundAttr、SrcAttr和TextColorAttr。这里以BackgroundAttr为例分析,其余两个同理。

public class BackgroundAttr extends BaseSkinAttr 

    public BackgroundAttr(String mAttrType, String mAttrName, String mAttrValueRef) 
        super(mAttrType, mAttrName, mAttrValueRef);
    

    @Override
    public boolean findResource() 
        resetResourceValue();

        if (TYPE_ATTR_DRAWABLE.equals(mAttrType)) 
            mFindDrawable = ResourceManager.getDefault().getDataResource().getDrawableByName(mAttrValueRef);
            return mFindDrawable != null;

         else if (TYPE_ATTR_COLOR.equals(mAttrType)) 
            mFindColor = ResourceManager.getDefault().getDataResource().getColorByName(mAttrValueRef);
            return mFindColor != Resource.VALUE_ERROR_COLOR;

        
        return true;
    

    @Override
    public void applySkin(View view) 
        if (TYPE_ATTR_DRAWABLE.equals(mAttrType) && mFindDrawable != null) 
            view.setBackgroundDrawable(mFindDrawable);
            SkinL.d(view + " : " + mAttrName + " apply " + mAttrValueRef);

         else if (TYPE_ATTR_COLOR.equals(mAttrType) && mFindColor != Resource.VALUE_ERROR_COLOR) 
            view.setBackgroundColor(mFindColor);
            SkinL.d(view + " : " + mAttrName + " apply " + mAttrValueRef);
        

        resetResourceValue();
    

在findResource中通过ResourceManager.getDefault().getDataResource().getXXXByName获取到mFindDrawable或者mFindColor。在applySkin方法中设置给View即可。到此,我们已经完整的看到了整个轮廓,现在唯一差的是ResourceManager.getDefault().getDataResource().getXXXByName的实现细节,这也是换肤的真正精髓所在。

本地换肤使用的是LocalResource对象。

public class LocalResource extends Resource 

    public LocalResource(Context baseSkinActivity, String pluginPackageName, String pluginPath, String resourcesSuffix) 
        super(baseSkinActivity, pluginPackageName, pluginPath, resourcesSuffix);
        mResources = baseSkinActivity.getResources();
    

    @Override
    public Drawable getDrawableByName(String drawableResName) 
        Drawable trueDrawable = null;
        drawableResName = appendSuffix(drawableResName);
        SkinL.d("getDrawableByName drawableResName:" + drawableResName);

        try 
            int trueDrawableId = mResources.getIdentifier(drawableResName, "drawable", GlobalManager.getDefault().getPackageName());
            trueDrawable = mResources.getDrawable(trueDrawableId);
         catch (Exception e) 
            e.printStackTrace();
        

        return trueDrawable;
    


public abstract class Resource 
    final String appendSuffix(String name) 
        if (!TextUtils.isEmpty(mResourcesSuffix)) 
            return name + "_" + mResourcesSuffix;
        

        return name;
    

前面说到:mResource.changeResourceSuffix(suffix)只是简单的记录night的后缀。现在就要用到这个night后缀了。在getDrawableByName中首先拼接名称,例如:将icon_search拼接为icon_search_night。之后通过mResources.getIdentifier获取到拼接后的资源的id。之后再转换成相应的Drawable。至此,本地换肤流程完毕。

动态换肤

动态换肤和本地换肤相比,换肤的流程是一样的,唯一的区别在于动态换肤的资源在另一个apk文件中。本地换肤是通过应用的Resources获取到相应的资源,那么动态换肤需要处理的问题就是怎么获取到外部apk的Resources对象。这个和插件化加载资源是一样的,构造外部apk的AssetManager即可。核心代码如下:

public class PluginResource extends Resource 

    public PluginResource(Context baseSkinActivity, String pluginPackageName, String pluginPath, String resourcesSuffix) throws Exception 
        super(baseSkinActivity, pluginPackageName, pluginPath, resourcesSuffix);
        loadPlugin();
    

    private void loadPlugin() throws Exception 
        File file = new File(PATH_EXTERNAL_PLUGIN + "/" + mPluginPath);
        SkinL.d(file.getAbsolutePath());
        if (mPluginPath == null || !file.exists()) 
            throw new IllegalArgumentException("plugin skin not exit, please check");
        

        AssetManager assetManager = null;

        assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, file.getAbsolutePath());

        Resources superRes = mContext.getResources();
        mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        SkinL.d("加载外部插件的皮肤成功");
    
    ...

AndSkin优点

  1. 支持应用内换肤和动态换肤
  2. 不干预View的生成

AndSkin缺/槽点

由于在ConfigConstants写死了一堆常量。所以xml中的SKIN_NAMES_SPACE必须为“http://schemas.android.com/android/andSkin”。同理写死,状态栏的颜色值必须为status_bar_color。

为了不干预View的生成,存储了View的id,也就意味这同一个布局文件中需要换肤的View都设置id属性并且不能重复。假设需要在代码中动态inflate10个xml,那你要写10个相同的xml,只是View的id不同。这点特别坑爹。。。

src加载应该暴露个接口,让用户可以使用图片加载框架加载。。。

目前就发现这么多,欢迎吐槽。。

以上是关于Andorid 换肤框架AndSkin源码解析及优缺点的主要内容,如果未能解决你的问题,请参考以下文章

Andorid 换肤框架AndSkin源码解析及优缺点

antd源码解析——在线换肤功能

antd源码解析——在线换肤功能

andorid jar/库源码解析 EventBus

插件式换肤框架搭建 - 资源加载源码分析

andorid jar/库源码解析之HotXposed