1、LayoutInflater解析xml布局
1.1、解析流程
解析xml的源码流程大概如下:
//LayoutInflater.java(android-29)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
// 1、用Factory2尝试创建view
View view = tryCreateView(parent, name, context, attrs);
// 2、创建失败则反射创建
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
// 用Factory2创建view
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
...
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
...
return view;
}
解析时显尝试用Factory2去创建,失败后才用反射的方式,一个目的是为了减少反射的调用,同时只要在合适的时机设置自定义的Factory2,就能拦截到所有要创建的view了。
1.2、设置Factory2
无论是用Fragment还是直接用Activity展示UI,拿到的LayoutInflater都是Activity的,因此直接关注Activity的创建生命周期。
// AppCompatActivity.java
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
...
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
...
}
// AppCompatDelegateImplV9.java
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
// 设置Factory2
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");
}
}
}
AppCompatActivity执行onCreate()时会创建AppCompatDelegate代理,AppCompatDelegate是个接口,installViewFactory()方法的实现类只有AppCompatDelegateImplV9,通过源码可以看到在installViewFactory()里执行了设置Factory2。AppCompatDelegateImplV9的实现是把一些=View做替换,如“TextView”替换成"AppCompatTextView"。因此当继承AppCompatActivity时,只需在super.onCreate()之前设置自定义Factory2,就能实现view创建的拦截。
小结:
- 自定义Factory2拦截view,执行换肤
- 在Activity super.onCreate()之前setFactory2(),让自定义的Factory2生效
2、装载皮肤包
2.1、装载
Android利用Resource(7.0?之后实现放到ResourceImpl中)、AssetManager来管理资源的获取。Resource的创建需要AssetManager实例,AssetManager提供了装载额外apk资源的方法:
// Android低版本
/**
* 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) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
// 高版本
/**
* Changes the asset paths in this AssetManager. This replaces the {@link #addAssetPath(String)}
* family of methods.
*
* @param apkAssets The new set of paths.
* @param invalidateCaches Whether to invalidate any caches. This should almost always be true.
* Set this to false if you are appending new resources
* (not new configurations).
* @hide
*/
public void setApkAssets(@NonNull ApkAssets[] apkAssets, boolean invalidateCaches) {}
// ApkAssets的创建方式
/**
* Creates a new ApkAssets instance from the given path on disk.
*
* @param path The path to an APK on disk.
* @param system When true, the APK is loaded as a system APK (framework).
* @return a new instance of ApkAssets.
* @throws IOException if a disk I/O error or parsing error occurred.
*/
public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system)
throws IOException {
return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/);
}
所以需要创建AssetManager对象,再调用以上方法把皮肤包apk路径传入,一个皮肤包对应一个Resource。
2.2、获取皮肤包的资源
一般获取资源需要通过指定id,如R.drawable.xxxx,但在不同的Resource中,相同的资源id不一定相同,即原apk的资源id和皮肤包apk的资源id不一致,因此无法直接用原apk的资源id,需要先找到该资源在皮肤apk里对应的id。Resource提供了对应的方法:
// Resource.java
// 1.获取EntryName
public String getResourceEntryName(@AnyRes int resid) throws NotFoundException;
// 2.获取TypeName
public String getResourceTypeName(@AnyRes int resid) throws NotFoundException;
// 3.根据EntryName、TypeName获取id
public int getIdentifier(String name, String defType, String defPackage);
具体代码为:
// 1.用原apk的Resource得到对应id的EntryName、TypeName
String originalResEntryName = originalResources.getResourceEntryName(originalResId);
String originalResTypeName = originalResources.getResourceTypeName(originalResId);
// 2.用插件apk的Resource和EntryName、TypeName反过来得到id
skinResId = pluginResources.getIdentifier(originalResEntryName, originalResTypeName, mPacketName);
// 3.用得到的id获取资源
Drawable drawable = pluginResources.getDrawable(skinResId)
因为是用资源名来实现资源查找,因此原apk里的资源名必须和皮肤apk里的资源名一致,否则在皮肤包中会找不到相应的id。
小结:
- 创建AssetManager对象,皮肤包apk路径作为参数调用指定方法(addAssetPath()/setApkAssets())
- 创建Resource对象管理该皮肤包的资源
- 根据原id得到资源名;根据资源名得到皮肤包内对应的id;根据id得到相应的资源