Hook一键换肤
- Hook是啥
- Hook的原理以及源码分析
- Hook代码实现
-
- 1.01 Hook以及hook一键换肤
- Hook是什么
- 1.01 Hook以及hook一键换肤
Hook别名钩子函数,在系统执行函数之前拦截函数并且执行自己的函数,这个过程又叫hook点,实现技术分别有两种
- 利用系统提供的接口,自己实现该接口并将执行代码注入到系统中
- 动态代理
-
- Android中的hook换肤
-
- 拦截系统的创建view的过程(内部创建view的过程与系统创建view的过程是一致的),在系统创建view的基础上标记需要换肤的view并收集。
- 换肤资源的加载(项目中使用接口连接下载换肤资源的apk)
- 使用PackageManager加载外部换肤资源的apk
- 构建加载外部apk的Resource以及使用反射创建AssetManager调用addAssetPath加载外部换肤apk(至于为啥要使用额外的手动构建的Resource加载外部apk,下面分析源码会讲到)
-
- 1.02 Android系统创建view源码分析
-
- (i) 由setContenView()着手,以下会贴出源代码片段:
-
- 1.02 Android系统创建view源码分析
1. AppCompatActivity.setContetnView()
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);//1.getWindow()是PhoneWindow
initWindowDecorActionBar();
}
2.PhoneWindow.setContentView
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent); //2. mLayoutInflater即是LayoutInflater
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
- LayoutInflater.inflate()绘制的起点,最终会调用
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);//createView的真正方法入口
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
首先会判断mFactory2是否为空,不为空则进行加载view,那么这个mFactory2具体在哪赋值?
首先从AppCompatActivity.setContentView
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
因为兼容性处理所有的AppCompatDelegate都继承自V9。
而在AppComaptActivity的onCreate方法有这么一句代码
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();//调用V9中的installViewFactory方法
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
AppCompatDelegateImpV9的installViewFactory()
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
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");
}
}
}
所以得知的是在类的onCreate最开始的时候就给mFactory2进行了赋值,由此可知LayoutInflater中的mFactory就是AppCompatActivity调用getDelegate得到的对象,两者就是一个,最后调用AppCompatDelegateImpV9.createView() -> AppCompatViewInflater.createView() ->
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 context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = 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 versions
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
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 its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
到此,view的创建过程就结束了,最后LayoutInflater创建的view是委托给了AppCompatViewInflater.createView()。
-
- 1.03 资源加载(需要自己创建Resource的原因会在这里讲解)
举个获取Drawable的例子:getResource().getColor(intresId)
ResourceImp.getValue() -> 通过AssetManager来获取获取TypeValue并给相关属性赋值
最后也是由Resourceimpl.loadColorStateList得到需要加载的颜色值(在颜色表单0xAARRGGBB中的颜色),至此,颜色的加载就完毕了,但是这是加载系统内也就是当前自己app内定义的资源color,但是加载外部apk资源呢??这时候看AssetManager里有一个hide方法:
/**
* 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) {
return addAssetPathInternal(path, false);
}
使用hide方法加载外部路径的资源-> 通过反射生成AassetManager调用addAssetPath加载外部资源。至于app安装类,资源的加载这里先不做分析。
-
- 1.04 一键换肤源码实现Demo
- 重写Factory2的onCreateView, 使用系统的Delegate首次去创建view, 如果创建的view为空,再次手动进行创建,保证创建view的代码与系统创建view的流程一致。
- 创建view之后,收集需要进行换肤操作的view,这里简单的定义一个属性isSupport来判断是否需要换肤,项目真实情况可以自己定义不同的属性来判断是否需要换肤以及需要进行替换的属性。
- 加载外部资源apk(需要替换的资源名一定要相同),因为相同资源名生成的资源id不一定相同,加载的时候先根据资源id获取到对应的资源名称,再由外部Resource根据资源名称去找到对应的资源id,这样才能保证替换资源是正确的。
自定义MyJavaFactory2 implements LayoutInflater.Factory2
package com.yjs.android.hook;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.v7.app.AppCompatDelegate;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.yjs.android.R;
import com.yjs.android.pages.main.MainActivity;
import com.yjs.android.view.tablayout.TabLayout;
import java.io.File;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
public class MyJavaFactory2 implements LayoutInflater.Factory2 {
public static final String TAG = "MyJavaFactory2";
private AppCompatDelegate appCompatDelegate;
public List<SkinView> skinViewList = new ArrayList<>();
private static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};//
//安卓里面控件的包名,就这么3种
private static final String[] sPreFix = new String[]{"android.widget.", "android.view.", "android.webkit."};
private static final HashMap<String, Constructor<? extends View>> sConstructorList = new HashMap<>();
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = appCompatDelegate.createView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (!name.contains(".")) {
view = createView(context, name, sPreFix, attrs);
}else view = createView(context, name, null, attrs);
}finally {
mConstructorArgs[0] = lastContext;
}
}
if(view != null) {
collectSkinView(view, context, attrs); //收集所有需要换肤的view
}
return view;
}
private final Object[] mConstructorArgs = new Object[2];//
/**
* 自己创建view的过程 通过反射
*/
private View createView(Context context, String name, String[] prefix, AttributeSet attrs) {
Constructor<? extends View> constructor = sConstructorList.get(name);
Class<? extends View> aClass = null;
if (constructor == null) {
try {
if(prefix != null && prefix.length > 0) {
for (String s : prefix) {
String fix;
if (TextUtils.isEmpty(s)) {
fix = name;
} else fix = s + name;
aClass = context.getClassLoader().loadClass(fix).asSubclass(View.class);
if(null != aClass) break;
}
}else {
aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
}
if(aClass == null) return null;
constructor = aClass.getConstructor(mConstructorSignature);
} catch (Exception e) {
e.printStackTrace();
return null;
}
sConstructorList.put(name, constructor);
constructor.setAccessible(true);
}
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
return constructor.newInstance(args);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//收集需要换肤的view
private void collectSkinView(View view, Context context, AttributeSet attrs) {
if(null == view || context == null) return;
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Skinable);
boolean support = typedArray.getBoolean(R.styleable.Skinable_isSupport, false);
if (support) {//支持换肤
int attributeCount = attrs.getAttributeCount();
HashMap<String, String> attrMap = new HashMap<>();
for (int i = 0; i < attributeCount; i++) {
String attributeName = attrs.getAttributeName(i);
String attributeValue = attrs.getAttributeValue(i);
Log.e("--" + view.getClass().getName() + "的属性", "name:" + attributeName + " value:"+ attributeValue);
attrMap.put(attributeName, attributeValue);
}
SkinView skinView = new SkinView(view, attrMap);
skinViewList.add(skinView);
}
typedArray.recycle();
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
private List<SkinView> getSkinViewList() {
return skinViewList;
}
public void setAppCompatDelegate(AppCompatDelegate delegate) {
this.appCompatDelegate = delegate;
}
public void changeSkin(String outResFilePath){
File file = new File(outResFilePath);
if(file.exists()) {
List<SkinView> skinViewList = getSkinViewList();
for (SkinView skinView : skinViewList) {
skinView.changeSkin();
}
}
}
class SkinView {
private View view;
private HashMap<String, String> attrMap;
SkinView(View view, HashMap<String, String> attrMap) {
this.view = view;
this.attrMap = attrMap;
}
public View getView() {
return view;
}
public HashMap<String, String> getAttrMap() {
return attrMap;
}
void changeSkin(){
String background = attrMap.get("background");
if(!TextUtils.isEmpty(background)){//换肤的控件有这个属性
//获取到 属性名字 @属性值
//background @2131230826
int backgroundId = Integer.parseInt(Objects.requireNonNull(background).substring(1)); //2131230826
String resourceTypeName = view.getResources().getResourceTypeName(backgroundId);//根据属性值获取到属性类别
if(TextUtils.equals("drawable", resourceTypeName)){//属性类别是backgroundDrawable
view.setBackground(SkinEngine.getInstance().getDrawable(backgroundId));
}else if(TextUtils.equals("color", resourceTypeName)){ //属性类别是backgroundColor
view.setBackgroundColor(SkinEngine.getInstance().getColor(backgroundId));
}
}
String textColor = attrMap.get("textColor");
if(view instanceof TextView){
if(!TextUtils.isEmpty(textColor)) {
int colorId = Integer.parseInt(Objects.requireNonNull(textColor).substring(1));
// String resourceTypeName = view.getResources().getResourceTypeName(colorId);
Log.e("---colorId", colorId + " ");
((TextView) view).setTextColor(SkinEngine.getInstance().getColor(colorId));
}
}
if(view instanceof ImageView){
String src = attrMap.get("src");
if(!TextUtils.isEmpty(src)){
int srcId = Integer.parseInt(Objects.requireNonNull(src).substring(1));
// String resourceTypeName = view.getResources().getResourceTypeName(srcId);
((ImageView) view).setImageDrawable(SkinEngine.getInstance().getSrcDrawable(srcId));
}
}
if(view instanceof TabLayout){
final int[] iconResourceIds = MainActivity.ICON_RESOURCE_IDS;
final int tabCount = ((TabLayout) view).getTabCount();
for(int i=0;i<tabCount;i++){
final View customView = Objects.requireNonNull(((TabLayout) view).getTabAt(i)).getCustomView();
ImageView image = Objects.requireNonNull(customView).findViewById(R.id.image_icon);
image.setImageDrawable(SkinEngine.getInstance().getSrcDrawable(iconResourceIds[i]));
}
}
}
}
}
定义加载外部资源apk,获取不同类型资源的工具类
package com.example.hook.hook;
public class SkinEngine {
private Context mContext;
private final static SkinEngine instance = new SkinEngine();
private Resources mOutResource;
private String mOutResPackageName;
private String mOutResFilePath;
private SkinEngine(){}
public static SkinEngine getInstance(){
return instance;
}
public void init(Context context){
this.mContext = context.getApplicationContext();
}
/**
* 加载外部资源
* @param outResPath 外部资源路径
*/
void load(String outResPath){
//通过路径加载外部加载外部apk 源码中resource中加载外部资源是由AssertManager addAssetPath()来加载的
//但是addAssetPath @hide 了
// /**
// * 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) {
// return addAssetPathInternal(path, false);
// }
//源码中创建resource的方法
// /**
// * Create a new Resources object on top of an existing set of assets in an
// * AssetManager.
// *
// * @deprecated Resources should not be constructed by apps.
// * See {@link android.content.Context#createConfigurationContext(Configuration)}.
// *
// * @param assets Previously created AssetManager.
// * @param metrics Current display metrics to consider when
// * selecting/computing resource values.
// * @param config Desired device configuration to consider when
// * selecting/computing resource values (optional).
// */
// @Deprecated
// public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
// this(null);
// mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
// }
// /storage/emulated/0/hook/hookres-debug.apk
File file = new File(outResPath);
if(file.exists()){
PackageManager packageManager = mContext.getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(outResPath, PackageManager.GET_ACTIVITIES);
if(packageArchiveInfo == null) return;
mOutResPackageName = packageArchiveInfo.packageName;
Log.e("--mOutResPackageName", mOutResPackageName);
AssetManager assetManager = null;
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, outResPath);
} catch (Exception e) {
e.printStackTrace();
}
mOutResource = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration());
mOutResFilePath = outResPath;
}
}
public String getmOutResFilePath() {
return mOutResFilePath;
}
Drawable getDrawable(int resId){ //根据资源id获取到对应的外部资源的id的drawable
//根据资源id获取到资源的名称 换肤时候相同名称的资源打包成apk的id可能是不一样的
String resourceEntryName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resourceEntryName, "drawable", mOutResPackageName);
if(outResId == 0){
return ContextCompat.getDrawable(mContext, resId);
}
return mOutResource.getDrawable(outResId, null);
}
Drawable getSrcDrawable(int resId){ //根据资源id获取到对应的外部资源的id的drawable
//根据资源id获取到资源的名称 换肤时候相同名称的资源打包成apk的id可能是不一样的
String resourceEntryName = mOutResource.getResourceEntryName(resId);//资源id对应的资源名称
int outResId = mOutResource.getIdentifier(resourceEntryName, "mipmap", mOutResPackageName);
if(outResId == 0){
return ContextCompat.getDrawable(mContext, resId);
}
return mOutResource.getDrawable(outResId, null);
}
int getColor(int resId){ //根据资源id获取到对应的外部资源的id的color
String resourceEntryName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resourceEntryName, "color", mOutResPackageName);
if(outResId == 0){
return ContextCompat.getColor(mContext, resId);
}
return mOutResource.getColor(outResId, null);
}
}
至此,换肤demo到此完成。