问题:
在插件中的xml布局中的自定义组件无法使用。
分析:
1、LayoutInflater.from(Context)创建LayoutInflater时,执行了Context的getSystemService方法。
LayoutInflater:
2、Context的getSystemService方法在Activity, ContextThemeWrapper中实现,在ContextThemeWrapper对LAYOUT_INFLATER_SERVICE服务进行处理,并通过mBaseContext来获取LayoutInflater
ContextThemeWrapper:
3、mBaseContext是在android.app.ContextImpl中处理LayoutInflater服务,所以每个activity/ContextWrapper只有一个LayoutIflater
ContextImpl:
private static final HashMap SYSTEM_SERVICE_MAP = new HashMap();
4、在插件中传的是Context是主工程的Context,LayoutInflater在ContextThemeWrapper中主工程的Context。LayoutInflater在解析布局文件中的自定义组件时,通过context.getClassLoader()来加载实例化,而context.getClassLoader()所得到的ClassLoader正是主工程的ClassLoader,其中不包含插件类。
LayoutInflater:
5、所以重点是要替换LayoutInflater解析自定义组件时所用到的ClassLoader,但是通过主工程context所得到的LayoutInflater最终都是会调用主工程的ClassLoader,而到导致无法实例化自定义组件。
解决方案:
通过创建新的Context并重写getClassLoader函数,返回插件ClassLoader。
1、Activity在父类ContextWrapper中通过mBaseContext获取ClassLoader,其中通过反射等方式替换主context的ClassLoader方式比较繁琐而且降低了安全性,所以可以选择创建一个新的插件Context。具体做法的是新建继承Context的JarContext类—>传入主Context和插件ClassLoader—>重写getClassLoader方法,并返回插件ClassLoader—>重新getSystemService方法,绑定一个LayoutInflater,保证一个JarContext只有一个LayoutInflater—>重写其他方法通过主工程context执行。
JarContext:
2、由于插件类是通过ClassLoader反射获取得到到的,所以插件ClassLoader的获取可以通过Class.getClassLoader获取得到。
3、LayoutInflater的构造函数处于protecte状态,所以一般都是通过context..getSystemService(Context.LAYOUT_INFLATER_SERVICE)得到,但是通过分析如此方法无法替换ClassLoader,所以我们需要创建一个新的LayoutInflater,创建LayoutInflater的时候传入JarContext,得到一个LayoutInflater实例。
4、最后通过跟JarContext绑定的LayoutInflater实例即可解析包含自定义组件的插件布局了。
弊端:
由于LayoutInflater的Context是新的JarContext,并且其ClassLoader是插件DexClassLoader,所以在解析布局的时候会将简写标签(如EditText)添加android.view前缀,但是EditText是andorid.widget包下的,所以ClassLoader加载时会报ClassNotFoundException异常。
解决办法:在layout布局中,给所有标签补全前缀,从而LayoutInflater解析的时候不会添加android.view前缀。
备注:每个Context对应唯一一个LayoutInFlater。