目录

1.什么是一键换肤

2.界面上那些东西可以换肤

3.利用Hook实现一键换肤

4.Android创建视图源码分析

4.1.自定义Activity设置要显示的布局文件xml

4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换

4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类

4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法

4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件

4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图

4.7.createViewFromTag()真正执行解析xml布局文件以后创建View视图

4.8.mFactory2来源(调试发现最终调用的是AppCompatActivity代理类的onCreateView()方法)

4.9.AppCompatViewInflater的createView()方法真正创建视图(AppCompatDelegateImplV9.onCreateView()调用AppCompatViewInflater.createView())

5.Resources/AssetManager

6.实现关键类SkinFactory,SkinEngine

6.1SkinFactory实现Factory2接口,完成视图的创建,视图的缓存,全部视图的换肤操作;

6.2SkinEngine加载插件apk的对应的Resources,(Resources用插件apk的资源)实现更换皮肤的各种方法(例如:getColor等)

6.3实例使用

6.4主app和插件app设置需要换肤资源用相同的名称

7.显示效果

8.注意事项


实现换肤的方案:

a.静态修改theme主题方式

设置多套皮肤的theme;

styles.xml
<resources><!-- Base application theme. --><style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"><!-- Customize your theme here. --><item name="defaultcolor">@color/defalultcolor</item>                                      </style><style name="NewTheme" parent="Theme.AppCompat.DayNight.NoActionBar"><item name="defaultcolor">@color/newcolor</item></style>
</resources>

声明属性需要动态替换的样式属性

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources><attr name="defaultcolor" format="reference|color" />
</resources>

为控件设置属性样式?attr/defaultcolor

<TextViewandroid:layout_marginTop="20dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="这是第一个fragment"android:textColor="?attr/defaultcolor"android:textSize="36dp" />

动态设置主题

@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);if( 1 ==  1){setTheme(R.style.AppTheme);}else {setTheme(R.style.NewTheme);}setContentView(R.layout.activity_main);
}

缺点是设置主题在setContentView之前,设置以后需要重启Activity;

b.动态修改样式(应用内/插件化-皮肤apk),不需要重启Activity

应用内

采用通过相同名称+不同皮肤后缀来区分不同皮肤,如实现黑白皮肤,文本颜色item_text_color有一套默认皮肤,一套黑色皮肤定义资源item_text_color,item_text_color_black;

String resName = mOutResource.getResourceEntryName(resId);int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);

resName+"_"+不同皮肤后缀

缺点:应用内多套皮肤时可能导致安装包过大;

插件化-皮肤apk

皮肤apk和主apk有相同皮肤资源名称,获取皮肤apk下的资源名称,需要和主apk下资源名称一致,设置在皮肤apk下皮肤资源 ;

String resName = mOutResource.getResourceEntryName(resId);int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);

mOutResource皮肤apk资源Resources;

动态修改视图皮肤需要解决两个问题:

1).缓存获取全部需要换肤的视图,换肤(资源)时执行换肤操作;

2).获取Resources资源(插件apk资源Resources),动态设置皮肤;

1.什么是一键换肤

所谓”一键换肤“就是通过一个接口调用,实现app范围内所有资源文件替换,包括文本,颜色,图片,动画等;

2.界面上那些东西可以换肤

例如TextView文字颜色,字体大小,ImageView的background等等;

res目录所有的资源几乎都可以替换,具体如下:

动画
背景图片
字体
字体颜色
字体大小
音频
视频

3.利用Hook实现一键换肤

什么是hook
如题,我是用hook实现一键换肤。那么什么是hook?
hook,钩子. 安卓中的hook技术,其实是一个抽象概念:对系统源码的代码逻辑进行"劫持",插入自己的逻辑,然后放行。注意:hook可能频繁使用java反射机制···

"一键换肤"中的hook思路

  1. "劫持"系统创建View的过程,我们自己来创建View
    系统原本自己存在创建View的逻辑,我们要了解这部分代码,以便为我所用.
  2. 收集我们需要换肤的View(用自定义view属性来标记一个view是否支持一键换肤),保存到变量中
    劫持了 系统创建view的逻辑之后,我们要把支持换肤的这些view保存起来
  3. 加载外部资源包,调用接口进行换肤
    外部资源包,是.apk后缀的一个文件,是通过gradle打包形成的。里面包含需要换肤的资源文件,但是必须保证,要换的资源文件,和原工程里面的文件名完全相同.

4.Android创建视图源码分析

自己定义Activity继承自兼容AppCompatActivity

public class MainActivity extends AppCompatActivity implements View.OnClickListener {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}
}

创建Activity经常在onCreate()方法中调用setContentView(R.layout.xxx);设置要显示视图,那么如何将我们创建的xml布局文件转换为要显示View视图呢?

源码执行流程:

4.1.自定义Activity设置要显示的布局文件xml

setContentView(R.layout.activity_main);

4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换

由于Android版本比较分散,需要兼容各个版本Android系统,AppCompatActivity定义实现不同版本AppCompatDelegate实现类;

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类

private static AppCompatDelegate create(Context context, Window window,AppCompatCallback callback) {final int sdk = Build.VERSION.SDK_INT;if (sdk >= 23) {return new AppCompatDelegateImplV23(context, window, callback);} else if (sdk >= 14) {return new AppCompatDelegateImplV14(context, window, callback);} else if (sdk >= 11) {return new AppCompatDelegateImplV11(context, window, callback);} else {return new AppCompatDelegateImplV7(context, window, callback);}}

4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法

通过如下实现将layout的xml布局文件转换为View视图添加到父视图上contentParent;

LayoutInflater.from(mContext).inflate(resId, contentParent);

4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件

4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {final AttributeSet attrs = Xml.asAttributeSet(parser);View result = root;//找到跟节点int type;while ((type = parser.next()) != XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {// Empty}if (type != XmlPullParser.START_TAG) {throw new InflateException(parser.getPositionDescription()+ ": No start tag found!");}//获取跟节点名字final String name = parser.getName();//创建xml的layout布局文件跟视图final View temp = createViewFromTag(root, name, inflaterContext, attrs);//递归调用创建子视图rInflateChildren(parser, temp, attrs, true);// 添加到xml布局文件视图到父视图if (root != null && attachToRoot) {root.addView(temp, params);}//将xml布局的跟视图添加做为父视图if (root == null || !attachToRoot) {result = temp;}...部分源码return result;}}

//创建xml布局文件的跟视图

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

//递归创建xml布局文件的子视图,最后调用createViewFromTag()方法创建视图

rInflateChildren(parser, temp, attrs, true);

createViewFromTag(parent, name, context, attrs);

4.7.createViewFromTag()真正执行解析xml布局文件以后创建View视图

最终调用mFactory2实现视图创建

view = mFactory2.onCreateView(parent, name, context, attrs);

if(view==null)

通过反射创建View(例如自定义View)

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {}

* @param parent:创建视图的父View
* @param name:xml标签使用的视图名称(例如:<TextView/>)
* @param context:上下文
* @param attrs:需要创建的xml标签的属性集;(例如:android:textColor="")
* @param ignoreThemeAttr:true忽略{android:theme}属性为被解析的视图,false相反;

4.8.mFactory2来源(调试发现最终调用的是AppCompatActivity代理类的onCreateView()方法)

AppCompatActivity设置LayoutInflater工厂

 @Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();delegate.installViewFactory();}

代理类AppCompatDelegateImplV7实现installViewFactory()方法

AppCompatDelegate(Activity代理类)
@Overridepublic void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {LayoutInflaterCompat.setFactory(layoutInflater, this);} else {if (!(LayoutInflaterCompat.getFactory(layoutInflater)instanceof AppCompatDelegateImplV7)) {Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"+ " so we can not install AppCompat's");}}}
LayoutInflaterCompat是LayoutInflater兼容类//LayoutInflater不同版本的实现static final LayoutInflaterCompatImpl IMPL;static {final int version = Build.VERSION.SDK_INT;if (version >= 21) {IMPL = new LayoutInflaterCompatImplV21();} else if (version >= 11) {IMPL = new LayoutInflaterCompatImplV11();} else {IMPL = new LayoutInflaterCompatImplBase();}}public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {IMPL.setFactory(inflater, factory);}static class LayoutInflaterCompatImplV11 extends LayoutInflaterCompatImplBase {@Overridepublic void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) {LayoutInflaterCompatHC.setFactory(layoutInflater, factory);}}FactoryWrapperHC实现了Factory2接口,包裹LayoutInflaterFactory接口,当调用Factroy2.
onCreateView方法时最后调用LayoutInflaterFactory(AppCompatDelegateImplV9).onCreateView()方法(class
AppCompatDelegateImplV9 extends AppCompatDelegateImplBaseimplements MenuBuilder.Callback, LayoutInflaterFactory),
AppCompatDelegateImplV9实现LayoutInflaterFactory接口
static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {final LayoutInflater.Factory2 factory2 = factory != null? new FactoryWrapperHC(factory) : null;//设置包裹LayoutInflaterFactory类做为factory2inflater.setFactory2(factory2);final LayoutInflater.Factory f = inflater.getFactory();if (f instanceof LayoutInflater.Factory2) {// The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).// We will now try and force set the merged factory to mFactory2forceSetFactory2(inflater, (LayoutInflater.Factory2) f);} else {// Else, we will force set the original wrapped Factory2forceSetFactory2(inflater, factory2);}}static class FactoryWrapperHC extends LayoutInflaterCompatBase.FactoryWrapperimplements LayoutInflater.Factory2 {FactoryWrapperHC(LayoutInflaterFactory delegateFactory) {super(delegateFactory);}@Overridepublic View onCreateView(View parent, String name, Context context,AttributeSet attributeSet) {return mDelegateFactory.onCreateView(parent, name, context, attributeSet);}}LayoutInfalter设置factory2/*** Like {@link #setFactory}, but allows you to set a {@link Factory2}* interface.*/public void setFactory2(Factory2 factory) {if (mFactorySet) {throw new IllegalStateException("A factory has already been set on this LayoutInflater");}if (factory == null) {throw new NullPointerException("Given factory can not be null");}mFactorySet = true;if (mFactory == null) {mFactory = mFactory2 = factory;} else {mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);}}

LayoutInflaterCompat.setFactory(layoutInflater, this);

将代理类AppCompatDelegateImplV7的this设置为mFactory2;

AppCompatDelegateImplV7实现了LayoutInflaterFactory接口,mFactory2调用onCreateView()方法最终调用实现Factory2接口FactoryWrapperHC类,FactoryWrapperHC包裹类LayoutInflaterFactory,最后调用LayoutInflaterFactory(AppCompatDelegateImplV7).onCreateView()方法创建视图;

4.9.AppCompatViewInflater的createView()方法真正创建视图(AppCompatDelegateImplV9.onCreateView()调用AppCompatViewInflater.createView())

 @Overridepublic View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs) {final boolean isPre21 = Build.VERSION.SDK_INT < 21;if (mAppCompatViewInflater == null) {mAppCompatViewInflater = new AppCompatViewInflater();}// We only want the View to inherit its context if we're running pre-v21final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */true, /* Read read app:theme as a fallback at all times for legacy reasons */VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */);}

AppCompatViewInflater

public final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {final Context originalContext = context;// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy// by using the parent's contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = themifyContext(context, attrs, readAndroidTheme, readAppTheme);}if (wrapContext) {context = TintContextWrapper.wrap(context);}View view = null;// We need to 'inject' our tint aware Views in place of the standard framework versionsswitch (name) {case "TextView":view = new AppCompatTextView(context, attrs);break;case "ImageView":view = new AppCompatImageView(context, attrs);break;case "Button":view = new AppCompatButton(context, attrs);break;case "EditText":view = new AppCompatEditText(context, attrs);break;case "Spinner":view = new AppCompatSpinner(context, attrs);break;case "ImageButton":view = new AppCompatImageButton(context, attrs);break;case "CheckBox":view = new AppCompatCheckBox(context, attrs);break;case "RadioButton":view = new AppCompatRadioButton(context, attrs);break;case "CheckedTextView":view = new AppCompatCheckedTextView(context, attrs);break;case "AutoCompleteTextView":view = new AppCompatAutoCompleteTextView(context, attrs);break;case "MultiAutoCompleteTextView":view = new AppCompatMultiAutoCompleteTextView(context, attrs);break;case "RatingBar":view = new AppCompatRatingBar(context, attrs);break;case "SeekBar":view = new AppCompatSeekBar(context, attrs);break;}if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check it's android:onClickcheckOnClickListener(view, attrs);}return view;}

创建Android的原生控件都是以AppCompat开头的兼容类例如:AppCompatSeekBar;

整个流程分析完成,我们发现最终调用mFactory2.onCreateView()和反射的方式创建视图;

就可以实现Factory2接口 ,给LayoutInflater重新设置实现自己逻辑Factory2(创建View视图,缓存视图和视图属性便于动态调用修改视图属性-文字颜色,字体大小,背景等)

5.Resources/AssetManager

调用AssetManager.addAssetPath("插件apk路径")指定要加载的皮肤apk,创建执行插件apk的Resources以便获取资源(图片,颜色,尺寸等);

需要动态设置皮肤资源就需要获取相关资源类(颜色,字体,背景等等),AssetManager.addAssetPath(String path)可以实现调用指定资源包;

Android中与资源相关的类主要有Resources、ResourcesImpl、ResourcesManager、Java层的AssetManager和Native层的AssetManager;

Resources:提供了大多数与应用开发直接相关的加载资源的方法,比如getColor(int resId)等。实现上是通过AssetManager来加载资源并进行解析。

AssetManager:Java层的AssetManager是对Native层AssetManager的封装,为上层提供了加载资源的方法。

加载资源包:一般而言,一个App至少会引用两个资源包:系统资源包和App的资源包。一个AssetManager对象可加载多个资源包。App在创建AssetManager的时候,会先加载系统资源包,再加载App资源包。这样,通过一个AssetManager对象就可以访问系统资源包和App资源包的资源了。

AssetManager.addAssetPath(String path)是一个@hide方法,调用这个方法可以加载指定的资源包。

访问资源:由于资源id包含了package id的信息,AssetManager通过解析资源id,即可知道从哪个资源包来加载资源。

6.实现关键类SkinFactory,SkinEngine

6.1SkinFactory实现Factory2接口,完成视图的创建,视图的缓存,全部视图的换肤操作;

/*** 自定义Factory2,目的是拿到需要换肤的View和View样式*/
public class SkinFactory implements LayoutInflater.Factory2 {//预定义一个委托类,他负责按照系统原有逻辑来创建Viewprivate AppCompatDelegate mDelegate;//缓存要换肤的Viewprivate List<SkinView> lisCacheSkinViews = new ArrayList<>();/*** 外部提供一个Set方法* @param mDelegate*/public void setDelegate(AppCompatDelegate mDelegate) {this.mDelegate = mDelegate;}/**** @param parent* @param name* @param context* @param attrs* @return*/@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {//关键点1:执行系统代码里的创建View的过程,我们只是想加入自己的思想,并不是全盘接管//系统创建出来的时候有可能为空,你问为啥?请全文搜索 “标记标记,因为” 你会找到你要的答案View view  = mDelegate.createView(parent, name, context, attrs);if(view == null){//万一系统创建出来是空,那我们来补救if(-1 == name.indexOf('.')){ //不包含,说明不带包名,那么我门帮他加上包名view = createViewByPrefix(context, name, prefixs, attrs);}else {view = createViewByPrefix(context, name, null, attrs);}}//关键点2:收集需要换肤的ViewcollectSkinView(context, attrs, view);return view;}/*** 收集换肤的控件* 收集的方式是:通过自定义isSupport,从创建出来的很多View中,找到支持换肤的那些,保存到map中* @param context* @param attrs* @param view*/private void collectSkinView(Context context, AttributeSet attrs, View view){//获取我们自己定义的属性TypedArray  typedArray = context.obtainStyledAttributes(attrs, R.styleable.Skinable);boolean isSupport = typedArray.getBoolean(R.styleable.Skinable_isSupport, false);if(isSupport){//找到支持换肤的Viewfinal int Len = attrs.getAttributeCount();HashMap<String,  String>  attrMap = new HashMap<>();for(int i=0; i< Len; i++){//遍历所有属性String attrName = attrs.getAttributeName(i);String attrValue = attrs.getAttributeValue(i);attrMap.put(attrName, attrValue);//全部存起来}SkinView skinView = new SkinView();skinView.view = view;skinView.attrsMap = attrMap;//将可换肤的View放到listCacheSkinView中lisCacheSkinViews.add(skinView);}}/*** 公开对外的换肤接口*/public void changeSkin(){for(SkinView skinView  : lisCacheSkinViews){skinView.changeSkin();}}/*** Factory2是继承Factory的,所以这次主要重写Factory2的onCreatteView逻辑,就不必理会Factory的重写方法* @param name* @param context* @param attrs* @return*/@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}/*** 所谓hook,要懂源码,懂了之后再劫持系统逻辑,加入自己的逻辑。* 那么,既然懂了,系统的有些代码,直接拿过来用,也无可厚非。*///*******************************下面一大片,都是从源码里面抄过来的,并不是我自主设计******************************// 你问我抄的哪里的?到 AppCompatViewInflater类源码里面去搜索:view = createViewFromTag(context, name, attrs);static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};//final Object[] mConstructorArgs = new Object[2];//View的构造函数的2个"实"参对象private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>();//用映射,将View的反射构造函数都存起来static final String[] prefixs = new String[]{//安卓里面控件的包名,就这么3种,这个变量是为了下面代码里,反射创建类的class而预备的"android.widget.","android.view.","android.webkit."};private final View createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs){Constructor<? extends View> constructor = sConstructorMap.get(name);Class<? extends View> clazz = null;if(constructor == null){try {if(prefixs != null && prefixs.length > 0){for(String prefix : prefixs){clazz = context.getClassLoader().loadClass(prefix != null ? (prefix+name) : name).asSubclass(View.class);if(clazz != null) break;}}else {if(clazz  == null){clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);}}if(clazz == null){return null;}//拿到构造方法constructor = clazz.getConstructor(mConstructorSignature);} catch (ClassNotFoundException e) {e.printStackTrace();return null;} catch (NoSuchMethodException e) {e.printStackTrace();return null;}constructor.setAccessible(true);//然后缓存起来,下次再用,就直接从内存中去取sConstructorMap.put(name, constructor);}Object[] args = mConstructorArgs;try {args[0] = context;args[1] = attrs;//通过反射创建View对象//执行构造函数,拿到View对下final View view = constructor.newInstance(args);return view;} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}finally {args[0] = null;args[1] = null;}return null;}static class  SkinView{View view;HashMap<String, String>  attrsMap;public void changeSkin(){if (!TextUtils.isEmpty(attrsMap.get("background"))) {//属性名,例如,这个background,text,textColor....int bgId = Integer.parseInt(attrsMap.get("background").substring(1));//属性值,R.id.XXX ,int类型,// 这个值,在app的一次运行中,不会发生变化String attrType = view.getResources().getResourceTypeName(bgId); // 属性类别:比如 drawable ,colorif (TextUtils.equals(attrType, "drawable")) {//区分drawable和colorview.setBackgroundDrawable(SkinEngine.getInstance().getDrawable(bgId));//加载外部资源管理器,拿到外部资源的drawable} else if (TextUtils.equals(attrType, "color")) {view.setBackgroundColor(SkinEngine.getInstance().getColor(bgId));}}if (view instanceof TextView) {if (!TextUtils.isEmpty(attrsMap.get("textColor"))) {int textColorId = Integer.parseInt(attrsMap.get("textColor").substring(1));((TextView) view).setTextColor(SkinEngine.getInstance().getColor(textColorId));}if (!TextUtils.isEmpty(attrsMap.get("textSize"))) {int textSizeId = Integer.valueOf(attrsMap.get("textSize").substring(1));((TextView) view).setTextSize(SkinEngine.getInstance().getTextSize(textSizeId));}}//            //那么如果是自定义组件呢
//            if (view instanceof ZeroView) {
//                //那么这样一个对象,要换肤,就要写针对性的方法了,每一个控件需要用什么样的方式去换,尤其是那种,自定义的属性,怎么去set,
//                // 这就对开发人员要求比较高了,而且这个换肤接口还要暴露给 自定义View的开发人员,他们去定义
//                // ....
//            }}}}

6.2SkinEngine加载插件apk的对应的Resources,(Resources用插件apk的资源)实现更换皮肤的各种方法(例如:getColor等)

/***  单例*/public class SkinEngine {private final static SkinEngine instance = new SkinEngine();public static SkinEngine getInstance(){return instance;}public void init(Context context){mContext = context.getApplicationContext();//使用application的目的是,如果万一传进来的是Activity对象//那么它被静态对象instance所持有,这个Activity就无法释放了}private Resources mOutResource; //资源管理器private Context mContext;  //上下文private String mOutPkgName; //外部资源包packageName//加载外部资源包public void load(final String path){    //path  是外部传入的apk文件名File file = new File(path);if(!file.exists()){return;}//取得PackageManager引用PackageManager mPm = mContext.getPackageManager();//“检索在包归档文件中定义的应用程序包的总体信息”,说人话,外界传入了一个apk的文件路径,这个方法,拿到这个apk的包信息,这个包信息包含什么?PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);mOutPkgName = mInfo.packageName; //先把包名存起来AssetManager assetManager;//资源管理器//关键技术点3 通过反射获取AssetManager 用来加载外面的资源包try {//反射创建AssetManager对象,为何要反射?使用反射,是因为他这个类内部的addAssetPath方法是hide状态assetManager = AssetManager.class.newInstance();//addAssetPath方法可以加载外部的资源包//为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);addAssetPath.invoke(assetManager, path); //反射执行方法mOutResource = new Resources(assetManager,  //资源管理器mContext.getResources().getDisplayMetrics(),    //屏幕参数mContext.getResources().getConfiguration());    //资源配置//最终创建出一个 "外部资源包"mOutResource ,它的存在,就是要让我们的app有能力加载外部的资源文件} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}/*** 提供外部资源包里面的颜色* @param resId* @return*/public int getColor(int resId) {if (mOutResource == null) {return resId;}String resName = mContext.getResources().getResourceEntryName(resId);int outResId = mOutResource.getIdentifier(resName, "color", mOutPkgName);if (outResId == 0) {return resId;}return mOutResource.getColor(outResId);}/*** 提供外部资源包里的图片资源* @param resId* @return*/public Drawable getDrawable(int resId) {//获取图片if (mOutResource == null) {return ContextCompat.getDrawable(mContext, resId);}String resName = mContext.getResources().getResourceEntryName(resId);int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);if (outResId == 0) {return ContextCompat.getDrawable(mContext, resId);}return mOutResource.getDrawable(outResId);}/*** 提供外部资源包里的字体大小* @param resId* @return*/public float getTextSize(int resId) {//获取字体大小if (mOutResource == null) {return mContext.getResources().getDimension(resId);}String resName = mContext.getResources().getResourceEntryName(resId);int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);if (outResId == 0) {return mContext.getResources().getDimension(resId);}return mOutResource.getDimension(outResId);}//..... 这里还可以提供外部资源包里的String,font等等等,只不过要手动写代码来实现getXX方法}

注意事项:

通过资源id找到资源名称,mContext是当前主app的上下文;

String resName = mContext.getResources().getResourceEntryName(resId);

mOutResource指的是插件apk的Resources获取和主app相同名称的资源替换显示;
int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);

6.3实例使用

初始化"换肤引擎"指定当前上下文

public class MyApp extends Application {@Overridepublic void onCreate() {super.onCreate();//初始化换肤引擎SkinEngine.getInstance().init(this);}
}

在基类Activity为LayoutInflator注入Facotry2类,其他Activity实现基类;

/*** Activity积类*/public class BaseActivity extends AppCompatActivity {private boolean ifAllowChangeSkin=true;private SkinFactory mSkinFactory;private String mCurrentSkin;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {// TODO: 关键点1:hook(劫持)系统创建view的过程if (ifAllowChangeSkin) {mSkinFactory = new SkinFactory();mSkinFactory.setDelegate(getDelegate());LayoutInflater layoutInflater = LayoutInflater.from(this);layoutInflater.setFactory2(mSkinFactory);//劫持系统源码逻辑}super.onCreate(savedInstanceState);}
}

在设置界面调用换肤操作, changeSkin("apk插件的名字"),具体apk插件存在哪和如何放入存储卡加载插件apk,下面仅仅是插件apk下载完成以后实现的方法;

changeSkin("skinmodule-debug.apk");
protected void changeSkin(String path) {if (ifAllowChangeSkin) {File skinFile = new File(Environment.getExternalStorageDirectory(), path);SkinEngine.getInstance().load(skinFile.getAbsolutePath());//加载外部资源包mSkinFactory.changeSkin();//执行换肤操作mCurrentSkin = path;}}

6.4主app和插件app设置需要换肤资源用相同的名称

6.5自定义isSupport属性设置View是否支持换肤

<resources><!--TODO: 关键技术点2 通过自定义属性来标识哪些view支持换肤--><declare-styleable name="Skinable"><!--TODO: isSupport=true标识当前控件支持换肤--><attr name="isSupport" format="boolean" /></declare-styleable>
</resources><TextViewandroid:layout_width="0dp"android:layout_weight="1"android:layout_height="match_parent"android:text="第一个"android:id="@+id/tv_one"android:gravity="center"android:textSize="@dimen/tabsize"app:isSupport="true"/>

7.显示效果

以下效果实现动态替换字体大小,背景颜色;

已经创建的Activity界面可能需要监听器,监听换肤操作执行换肤,新创建的页面将使用插件apk皮肤资源;

8.注意事项

a.skinmodule创建module只需要管理assets和res下的资源即可,需要替换资源名称要和app一致;

b.皮肤包 skin_plugin module的gradle sdk版本最好和app module的保持完全一致,否则无法保证不会出现奇葩问题;

c.skinmodule生成的具体保存位置自己定义,只需要创建插件Resources时执行插件路径apk即可;

参考:

https://www.jianshu.com/p/4c8d46f58c4f

https://github.com/18598925736/HookSkinDemoFromHank

Android通过Hook技术实现一键换肤相关推荐

  1. opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤

    前言 产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数Str ...

  2. Android hook技术实现一键换肤,移动应用开发项目案例

    3. 利用HOOK技术实现优雅的"一键换肤" 什么是hook **如题,我是用hook实现一键换肤.那么什么是hook? hook,钩子. 安卓中的hook技术,其实是一个抽象概念 ...

  3. wegame一键蹲替换文件_手把手讲解 Android hook技术实现一键换肤

    前言 产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数Str ...

  4. android换肤的实现方案,一种基于Android平台的一键换肤方法与流程

    本发明属于互联网技术领域,尤其涉及一种基于Android平台的一键换肤方法. 背景技术: 换肤性能的添加在很大的程度上面提高了用户的体验感,用户可以根据自己喜好选择自己喜欢的皮肤颜色,并且我们针对不同 ...

  5. Android一键换肤原理简述

    简介 Android对应用进行换肤操作,首先要生成一个对应的皮肤包,在要换肤的应用中收集需要换肤的控件,获取皮肤包里的资源,一键换肤时遍历View树,对要换肤的控件进行换肤.下面总结为4个步骤 步骤 ...

  6. android换肤的实现方案,Android应用开发之Android一键换肤功能实现

    本文将带你了解Android应用开发之Android一键换肤功能实现,希望本文对大家学Android有所帮助. < 市面上对数的App都提供换肤功能,这里暂且不讲白天和夜间模式 下图是网易云音乐 ...

  7. 三步实现Android应用内一键换肤,无需重启应用,无需编写多余代码

    古人学问无遗力,少壮工夫老始成. 纸上得来终觉浅,绝知此事要躬行. --<冬夜读书示子聿> · 陆游 换肤效果演示 四种皮肤切换:默认白色.蓝色.绿色和玫瑰红: 前言 先给大家介绍一下现在 ...

  8. Android插件化的思考——仿QQ一键换肤,思考比实现更重要!

    Android插件化的思考--仿QQ一键换肤,思考比实现更重要! 今天群友希望写一个关于插件的Blog,思来想去,插件也不是很懂,只是用大致的思路看看能不能模拟一个,思路还是比较重要的,如果你有兴趣的 ...

  9. android 仿qq换肤功能,Android插件化的思考——仿QQ一键换肤,思考比实现更重要!.doc...

    Android插件化的思考--仿QQ一键换肤,思考比实现更重要! 关于QQ的换肤,他们的实现思路我不是很清楚,但是你可以看一下这张换肤的截图 我们想使用哪个主题就直接下载就好了,这一实现的过程我们大致 ...

最新文章

  1. 40岁学python有前途吗-西安新城区学python人工智能少儿编程哪家机构好
  2. IOS-Core Data的使用
  3. java反编译工具_ReverseTool逆向工具集合
  4. OllyDbg笔记-软件逆向调试技巧
  5. mac os x 10.8 安装python-mysqldb
  6. 制作个性化gurb菜单背景图片
  7. linux复制文件属性不变例子,linux中文件系统属性chattr权限
  8. 【073】Android 数据存储(SQLite)
  9. 来自微信团队的6个开源项目
  10. vc c mysql_VC++ 利用 MySQL connector c 访问MySQL 数据库
  11. MFC Windows 程序设计[五]之绘制表格Accel
  12. 行政区划代码(道路要素)
  13. repo sync error: Exited sync due to fetch errors
  14. BIOS锁定纯UEFI启动的解锁办法
  15. JNDI全攻略(一)
  16. 鸿蒙西游安卓版,鸿蒙西游安卓版_鸿蒙西游V1.0.0_游戏堡
  17. cin、cout、cerr、clog用法区别及其在VS环境下的重定向
  18. 英雄联盟LPL比赛数据可视化详细教程,可视化的魅力,你值得拥有!!!
  19. 王选院士谈院士增选:院士未必总是学术权威
  20. Qt Quick 渲染机制

热门文章

  1. 26、Java——迷你图书管理器(对象+IO流)
  2. jquery mobile android浏览器,使用JQuery Mobile实现手机新闻浏览器
  3. 分享76个ASP其他类别源码,总有一款适合您
  4. npm设置镜像如淘宝:http://npm.taobao.org/
  5. WEB前端全套零基础视频教程+软件2021最新编程视频需要留邮箱
  6. 永远的金大侠-人工智能的江湖
  7. linux 音频转换工具,Ubuntu 14.04安装音频转换工具SoundConverter 2.1.3
  8. Lwip之TCP协议实现(二)
  9. 乐观锁,悲观锁,死锁
  10. 购买一家数码电子天猫转让网店价格