1、引言
1.插件化的特点
应用在运行的时候通过加载一些本地不存在的可执行文件实现一些特定的功能;Android中动态加载的核心思想是动态调用外部的 dex文件。
2.插件化需要解决的问题
插件包中代码和资源的加载问题
3.插件化必备基础
ClassLoader类加载器 、Java反射 、插件资源访问 、代理模式
2、例子
分析一个插件式换肤的源码来了解插件资源访问。
源码下载地址:https://github.com/ximsfei/Android-skin-support
3、源码分析
3.1 LayoutInflater简介
LayoutInflater 的作用就是将XML布局文件实例化为相应的 View 对象,需要通过Activity.getLayoutInflater() 或 Context.getSystemService(Class) 来获取与当前Context已经关联且正确配置的标准LayoutInflater。
过程如下:
通过 XML 的 Pull 解析方式获取 View 的标签。
通过标签以反射的方式来创建 View 对象。
如果是 ViewGroup 的话则会对子 View 遍历并重复以上步骤,然后 add 到父 View 中。
与之相关的几个方法:inflate ——》 rInflate ——》 createViewFromTag ——》 createView。
/**
* createViewFromTag 方法比较简单,首先尝试通过 Factory 来创建View;
* 如果没有 Factory 的话则通过 createView 来创建View;
**/
ViewcreateViewFromTag(Viewparent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
Viewview;
if (mFactory2 != null) {
//有mFactory2,则调用mFactory2的onCreateView方法
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
// 有mFactory,则调用mFactory的onCreateView方法
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
//有mPrivateFactory,则调用mPrivateFactory的onCreateView方法
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
// ④ 走到这步说明三个Factory都没有,则开始自己创建View
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
// ⑤ 如果View的name中不包含 '.' 则说明是系统控件,会在接下来的调用链在name前面加上 'android.view.'
view = onCreateView(parent, name, attrs);
} else {
// ⑥ 如果name中包含 '.' 则直接调用createView方法,onCreateView 后续也是调用了createView
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
3.2 LayoutInflater.Factory
简介:通过 LayoutInflater 创建View时候的一个回调,可以通过LayoutInflater.Factory来改造 XML 中存在的 tag。
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
设置setFactory方法:
Activity的onCreate里的super.onCreate之前调用setFactory方法。
利用反射修改LayoutInflater里面的mFactorySet变量。
private void installLayoutFactory(Context context) {
LayoutInflater layoutInflater = LayoutInflater.from(context);
try {
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
3.3 获取插件包中的资源
方法一:
/**
* 获取皮肤包资源{@link Resources}.
*
* @param skinPkgPath sdcard中皮肤包路径.
* @return
*/
@Nullable
public Resources getSkinResources(String skinPkgPath) {
try {
PackageInfo packageInfo = mAppContext.getPackageManager().getPackageArchiveInfo(skinPkgPath, 0);
packageInfo.applicationInfo.sourceDir = skinPkgPath;
packageInfo.applicationInfo.publicSourceDir = skinPkgPath;
Resources res = mAppContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
Resources superRes = mAppContext.getResources();
return new Resources(res.getAssets(), superRes.getDisplayMetrics(), superRes.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
方法二:
/**
* @return 得到对应插件的Resource对象
*/
@Nullable
public Resources getSkinResources(String skinPkgPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = mAppContext.getResources();
Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
return mResources;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
3.4换肤
源码中可以通过拦截View创建过程, 替换一些基础的组件(比如TextView -> AppCompatTextView), 然后对一些特殊的属性(比如:background, textColor) 做处理, 那我们为什么不能将这种思想拿到换肤框架中来使用呢?我擦,一语惊醒梦中人啊,老哥.我们也可以搞一个委托啊,我们也可以搞一个类似于AppCompatViewInflater的控件加载器啊,我们也可以设置mFactory2啊,相当于创建View的过程由我们接手.
/**
* 初始化换肤框架. 通过该方法初始化,应用中Activity需继承自{@link skin.support.app.SkinCompatActivity}.
*
* @param context
* @return
*/
public static SkinCompatManager init(Context context) {
if (sInstance == null) {
synchronized (SkinCompatManager.class) {
if (sInstance == null) {
sInstance = new SkinCompatManager(context);
}
}
}
SkinPreference.init(context);
return sInstance;
}
public static SkinCompatManager getInstance() {
return sInstance;
}
/**
* 初始化换肤框架,监听Activity生命周期. 通过该方法初始化,应用中Activity无需继承{@link skin.support.app.SkinCompatActivity}.
*
* @param application 应用Application.
* @return
*/
public static SkinCompatManager withoutActivity(Application application) {
init(application);
SkinActivityLifecycle.init(application);
return sInstance;
}
public static SkinActivityLifecycle init(Application application) {
if (sInstance == null) {
synchronized (SkinActivityLifecycle.class) {
if (sInstance == null) {
sInstance = new SkinActivityLifecycle(application);
}
}
}
return sInstance;
}
private SkinActivityLifecycle(Application application) {
application.registerActivityLifecycleCallbacks(this);
installLayoutFactory(application);
SkinCompatManager.getInstance().addObserver(getObserver(application));
}
SkinCompatDelegate实现LayoutInflaterFactory,这里hook:
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createView(parent, name, context, attrs);
if (view == null) {
return null;
}
if (view instanceof SkinCompatSupportable) {
mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
}
return view;
}
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mSkinCompatViewInflater == null) {
mSkinCompatViewInflater = new SkinCompatViewInflater();
}
...
return mSkinCompatViewInflater.createView(parent, name, context, attrs);
}
public static SkinCompatDelegate create(Context context) {
return new SkinCompatDelegate(context);
}
SkinCompatViewInflater透传到各个子控件的Inflater处理,自定义子控件的Inflater是在Application里面设置的。
public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
view = createViewFromInflater(context, name, attrs);
if (view == null) {
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
private View createViewFromInflater(Context context, String name, AttributeSet attrs) {
View view = null;
for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {
view = inflater.createView(context, name, attrs);
if (view == null) {
continue;
} else {
break;
}
}
return view;
}
注:为什么要在SkinCompatViewInflater还要细化,还需要交由更细的SkinLayoutInflater来处理呢?
—-方便扩展,库中给出了几个SkinLayoutInflater,有SkinAppCompatViewInflater(基础控件构建器)、SkinMaterialViewInflater(material design控件构造器)、SkinConstraintViewInflater(ConstraintLayout构建器)、SkinCardViewInflater(CardView v7构建器)。
SkinAppCompatViewInflater :基础组件的Inflater
@Override
public View createView(Context context, String name, AttributeSet attrs) {
View view = createViewFromFV(context, name, attrs);
if (view == null) {
view = createViewFromV7(context, name, attrs);
}
return view;
}
private View createViewFromFV(Context context, String name, AttributeSet attrs) {
View view = null;
if (name.contains(".")) {
return null;
}
switch (name) {
case "View":
view = new SkinCompatView(context, attrs);
break;
case "LinearLayout":
view = new SkinCompatLinearLayout(context, attrs);
break;
case "RelativeLayout":
view = new SkinCompatRelativeLayout(context, attrs);
break;
case "FrameLayout":
view = new SkinCompatFrameLayout(context, attrs);
break;
case "TextView":
view = new SkinCompatTextView(context, attrs);
break;
case "ImageView":
view = new SkinCompatImageView(context, attrs);
break;
case "Button":
view = new SkinCompatButton(context, attrs);
break;
case "EditText":
view = new SkinCompatEditText(context, attrs);
break;
case "Spinner":
view = new SkinCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new SkinCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new SkinCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new SkinCompatRadioButton(context, attrs);
break;
case "RadioGroup":
view = new SkinCompatRadioGroup(context, attrs);
break;
case "CheckedTextView":
view = new SkinCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new SkinCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new SkinCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new SkinCompatSeekBar(context, attrs);
break;
case "ProgressBar":
view = new SkinCompatProgressBar(context, attrs);
break;
case "ScrollView":
view = new SkinCompatScrollView(context, attrs);
break;
default:
break;
}
return view;
}
AppCompatActivity的实现,将background相关的属性交给SkinCompatBackgroundHelper去处理,将textColor相关的操作交给SkinCompatTextHelper去处理。该换肤源码也是这样设计的。并且每个支持换肤的View实现SkinCompatSupportable接口,比如:
public class SkinCompatView extends View implements SkinCompatSupportable {
private SkinCompatBackgroundHelper mBackgroundTintHelper;
public SkinCompatView(Context context) {
this(context, null);
}
public SkinCompatView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SkinCompatView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
}
@Override
public void setBackgroundResource(int resId) {
super.setBackgroundResource(resId);
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.onSetBackgroundResource(resId);
}
}
@Override
public void applySkin() {
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySkin();
}
}
}
public interface SkinCompatSupportable {
void applySkin();
}
3.5总结:
监听APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
在每个Activity的onCreate()方法调用时setFactory(),设置创建View的工厂.将创建View的琐事交给SkinCompatViewInflater去处理.
库中自己重写了系统的控件(比如View对应于库中的SkinCompatView),实现换肤接口(接口里面只有一个applySkin()方法),表示该控件是支持换肤的.并且将这些控件在创建之后收集起来,方便随时换肤.
在库中自己写的控件里面去解析出一些特殊的属性(比如:background, textColor),并将其保存起来
在切换皮肤的时候,遍历一次之前缓存的View,调用其实现的接口方法applySkin(),在applySkin()中从皮肤资源(可以是从网络或者本地获取皮肤包)中获取资源.获取资源后设置其控件的background或textColor等,就可实现换肤.
(完)