在上一篇《framework插件化技術-類加載》說到了一種framework特性插件化技術中的類加載方式解決lib的動態加載問題,可是咱們都知道在Android裏面,除了代碼意外,還有一塊很是重要的領域就是資源。
plugin裏的代碼是不能按照普通的方式直接加載資源的,由於plugin裏拿到的context都是從app傳過來的,若是按照普通的方式加載資源,加載的都是app的資源,沒法加載到plugin apk裏的資源。因此咱們須要在plugin的feature和res中間加入一箇中間層:PluginResLoader,來專門負責加載資源,結構以下:java
咱們都知道在Android中Resources類是專門負責加載資源的,因此PluginResLoader的首要任務就是構建Resources。咱們平時在Activity中經過getResources()的接口獲取Resources對象,可是這裏獲取到的Resources是與應用Context綁定的Resources,咱們須要構造plugin的Resources,這裏咱們能夠看下如何構造Resources:android
咱們能夠看到構造函數裏須要傳入三個參數,AssetManager,DisplayMetrics,Configuration。後面兩個參數咱們能夠經過app傳過來的context得到,因此如今問題能夠轉換爲構造AssetManager。咱們發現AssetManager的構造函數並不公開,因此這裏只能經過反射的方式構造。同時AssetManager還有一個接口能夠直接傳入資源的路徑:public int addAssetPath(String path)
複製代碼
因此,綜上所述,咱們能夠獲得構造Resources的方法:
PluginResLoader:web
public Resources getResources(Context context) {
AssetManager assetManager = null;
try {
mPluginPath = context.getDataDir().getPath() + "/plugin.apk";
assetManager = AssetManager.class.newInstance();
if(assetManager != null) {
try {
AssetManager.class.getDeclaredMethod("addAssetPath", String.class)
.invoke(assetManager, mPluginPath);
} catch (InvocationTargetException e) {
assetManager = null;
e.printStackTrace();
} catch (NoSuchMethodException e) {
assetManager = null;
e.printStackTrace();
}
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Resources resources;
if(assetManager != null) {
resources = new Resources(assetManager,
context.getResources().getDisplayMetrics(),
context.getResources().getConfiguration());
} else {
resources = context.getResources();
}
return resources;
}
複製代碼
這樣,咱們就能夠經過構造出來的Resources訪問plugin上的資源。 可是在實際的操做中,咱們發現獲取了Resources還不能解決全部的資源訪問問題。style和layout依然沒法直接獲取。bash
因爲plugin不是已安裝的apk,因此咱們不能使用Resources的getIdentifier接口來直接獲取資源id。可是咱們知道,apk的資源在編譯階段都會生成R文件,因此咱們能夠經過反射plugin apk中的R文件的方式來獲取id:app
/**
* get resource id through reflect the R.java in plugin apk.
* @param context the base context from app.
* @param type res type
* @param name res name
* @return res id.
*/
public int getIdentifier(Context context, String type, String name) {
if(context == null) {
Log.w(TAG, "getIdentifier: the context is null");
return -1;
}
if(RES_TYPE_STYLEABLE.equals(type)) {
return reflectIdForInt(context, type, name);
}
return getResources(context).getIdentifier(name, type, PLUGIN_RES_PACKAGE_NAME);
}
/**
* get resource id array through reflect the R.java in plugin apk.
* eg: get {@link R.styleable}
* @param context he base context from app.
* @param type res type
* @param name res name
* @return res id
*/
public int[] getIdentifierArray(Context context, String type, String name) {
if(context == null) {
Log.w(TAG, "getIdentifierArray: the context is null");
return null;
}
Object ids = reflectId(context, type, name);
return ids instanceof int[] ? (int[])ids : null;
}
private Object reflectId(Context context, String type, String name) {
ClassLoader classLoader = context.getClassLoader();
try {
String clazzName = PLUGIN_RES_PACKAGE_NAME + ".R$" + type;
Class<?> clazz = classLoader.loadClass(clazzName);
if(clazz != null) {
Field field = clazz.getField(name);
field.setAccessible(true);
return field.get(clazz);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return -1;
}
private int reflectIdForInt(Context context, String type, String name) {
Object id = reflectId(context, type, name);
return id instanceof Integer ? (int)id : -1;
}
複製代碼
咱們都知道,獲取layout資源沒法直接經過Resources拿到,而須要經過LayoutInflater來獲取。通常的方法:ide
LayoutInflater inflater = LayoutInflater.from(context);
複製代碼
而這裏穿進去的context是應用傳進來的context,很明顯經過應用的context咱們是沒法獲取到plugin裏的資源的。那麼咱們如何獲取plugin裏的layout資源呢?是否須要構造一個本身的LayoutInflater和context?咱們進一步去看一下源碼:函數
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
複製代碼
咱們能夠看到是經過context的getsystemService方法來獲取的LayoutInflater。進入到Context的源碼咱們能夠看到Context是一個抽象類,並無getSystemService的實現。那這個實現到底在那裏呢?這裏就不得不去研究一下Context的整個結構。佈局
@Override
public Object getSystemService(String name) {
return mBase.getSystemService(name);
}
複製代碼
最終會經過mBase來獲取。一樣的咱們還能夠看到ContextWrapper裏不少其餘的方法也是代理給mBase去實現的。很明顯這是一個裝飾模式。ContextWrapper只是一個Decorator,真正的實如今mBase。那麼咱們看下mBase在哪裏建立:post
public ContextWrapper(Context base) {
mBase = base;
}
/**
* Set the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Throws
* IllegalStateException if a base context has already been set.
*
* @param base The new base context for this wrapper.
*/
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
複製代碼
這裏咱們能夠看到有兩處能夠傳進來,一處是構造函數裏,還有一處是經過attachBaseContext方法傳進來。因爲咱們這裏的lib主要針對Activity實現,因此咱們須要看一下在Activity建立伊始,mBase是如何構建的。
咱們都知道,Activity的建立是在ActivityThread的performLaunchActivity方法中:ui
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
//建立base context
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
...
//建立activity
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
} catch (Exception e) {
...
}
...
if (activity != null) {
...
//綁定base context到activity
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
...
}
...
}
複製代碼
在上述代碼中,建立了一個ContextImpl對象,以及一個Activity對象,而且將ContextImpl做爲base context經過activity的attach方法傳給了Activity:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
...
}
複製代碼
還記得咱們在前面提到的ContextWrapper中構建mBase的兩個地方,一個是構造函數,還有一個即是attachBaseContext方法。因此至此咱們能夠解答前面疑惑的mBase對象是什麼了,原來就是在ActivityThread建立Activity的同時建立的ContextImpl對象。也就是說,其實contexImpl是context的真正實現。
回到咱們前面討論的getSystemService問題,咱們到/frameworks/base/core/java/android/app/ContextImpl.java中看:
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
複製代碼
再轉到/frameworks/base/core/java/android/app/SystemServiceRegistry.java,咱們找到註冊LayoutInflater服務的地方:
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
複製代碼
這裏咱們能夠看到系統的LayoutInflater實現實際上是PhoneLayoutInflater。這樣好辦了。咱們能夠仿造PhoneLayoutInflater構造一個PluginLayoutInflater就行了。可是在前面的討論中咱們知道LayoutInflater是沒法直接建立的,而是經過Context間接建立的,因此這裏咱們還須要構造一個PluginContext,仿造原有的Context的方式,在getSystemService中返回咱們的PluginLayoutInflater。具體實現以下:
PluginContextWrapper:
public class PluginContextWrapper extends ContextWrapper {
private Resources mResource;
private Resources.Theme mTheme;
public PluginContextWrapper(Context base) {
super(base);
mResource = PluginResLoader.getsInstance().getResources(base);
mTheme = PLuginResLoader.getsInstance().getTheme(base);
}
@Override
public Resources getResources() {
return mResource;
}
@Override
public Resources.Theme getTheme() {
return mTheme;
}
@Override
public Object getSystemService(String name) {
if(Context.LAYOUT_INFLATER_SERVICE.equals(name)) {
return new PluginLayoutInflater(this);
}
return super.getSystemService(name);
}
}
複製代碼
PluginLayoutInflater:
public class PluginLayoutInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget",
"android.webkit",
"android.app"
};
protected PluginLayoutInflater(Context context) {
super(context);
}
protected PluginLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new PluginLayoutInflater(this, newContext);
}
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
View view = createView(name, prefix, attrs);
if(view != null) {
return view;
}
}
return super.onCreateView(name, attrs);
}
}
複製代碼
PluginResLoader:
public Context getContext(Context base) {
return new PluginContextWrapper(base);
}
複製代碼
這樣在plugin中若是須要加載佈局只須要以這樣的方式便可加載:
LayoutInflater inflater = LayoutInflater.from(
PluginResLoader.getsInstance().getContext(context));
複製代碼
其中應用的傳過來的context能夠做爲pluginContext的base,這樣咱們能夠獲取不少應用的信息。
咱們都知道主題是一系列style的集合。而通常咱們設置主題的範圍是app或者是Activity,可是若是咱們只純粹但願Theme單純應用於咱們的lib,而咱們的lib並非一個app或者是一個Activity,咱們但願咱們的lib可以有一個統一的風格。那怎麼樣構建主題而且應用與plugin呢? 首先咱們須要看下在Google的原聲控件裏是如何讀取主題定義的屬性的,好比在 /frameworks/base/core/java/android/widget/Toolbar.java 控件裏是這樣讀取的:
public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Toolbar,
defStyleAttr, defStyleRes);
...
}
複製代碼
這裏的attrs在兩參構造函數裏傳進來:
public Toolbar(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.toolbarStyle);
}
複製代碼
這裏咱們讀到兩個關鍵信息,Toolbar的風格屬性是toolbarStyle,而控件經過屬性解析資源的方式是經過context.obtainStyledAttributes拿到TypedArray來獲取資源。
那Google又是在哪裏定義了toolbarStyle的呢?查看Goolge的資源代碼,咱們找到在/frameworks/base/core/res/res/values/themes_material.xml 中:
<style name="Theme.Material">
...
<item name="toolbarStyle">@style/Widget.Material.Toolbar</item>
...
</style>
複製代碼
在Theme.Materail主題下定義了toolbarStyle的風格。這裏順便提一下,/frameworks/base/core/res/res/values/themes_material.xml 是Google專門爲material風格設立的主題文件,固然values下的全部文件均可以合爲一個,可是很明顯這樣分開存儲會在代碼結構上清晰許多。一樣的在/frameworks/base/core/res/res/values/themes.xml 文件下的Theme主題下也定義了toolbarStyle,這是Android的默認主題,也是全部主題的祖先。關於toolbarStyle的各個主題下的定義,這裏就不一一列舉了,感興趣的童鞋能夠直接到源碼裏看。 到這裏framework把控件接口作好,應用只須要在AndroidManifest.xml文件裏配置Activity或者Application的主題:
<activity android:name=".MainActivity"
android:theme="@android:style/Theme.Material">
複製代碼
就能夠將Activity界面應用於Material主題,從而Toolbar控件就會選取Material主題配置的資源來適配。這樣,就能夠達到資源和代碼的徹底解耦,不須要改動代碼,只須要配置多套資源(好比設置Holo主題,Material主題等等),就可讓界面顯示成徹底不一樣的樣式。
如今咱們回頭看context.obtainStyleAtrributes方法,咱們去具體看下這個方法的實現:
public final TypedArray obtainStyledAttributes(
AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr,
@StyleRes int defStyleRes) {
return getTheme().obtainStyledAttributes(
set, attrs, defStyleAttr, defStyleRes);
}
複製代碼
這裏能夠看到最終是經過getTheme來實現的,也就是最終解析屬性是交給Theme來作的。這裏就能夠看到和主題的關聯了。那麼咱們繼續往下看下獲取到的主題是什麼,Activity的getTheme實如今/frameworks/base/core/java/android/view/ContextThemeWrapper.java:
@Override
public Resources.Theme getTheme() {
...
initializeTheme();
return mTheme;
}
複製代碼
這裏theme的建立在initializeTheme方法裏:
private void initializeTheme() {
final boolean first = mTheme == null;
if (first) {
//建立主題
mTheme = getResources().newTheme();
...
}
//適配特定主題style
onApplyThemeResource(mTheme, mThemeResource, first);
}
protected void onApplyThemeResource(Resources.Theme theme, int resId, boolean first) {
theme.applyStyle(resId, true);
}
複製代碼
這裏咱們就能夠看到theme的建立是經過Resources的newTheme()方法來建立的,而且經過theme.applyStyle方法將對應的theme資源設置到theme對象中。
至此,咱們已經知道如何構建一個theme了,那麼怎麼獲取themeId呢?
咱們知道framework是經過讀取Activity或Application設置的theme白噢錢來設置theme對象的,那咱們的plugin是否也能夠在AndroidManifest.xml文件裏讀取這樣相似的標籤呢?答案是確定的。
在AndroidManifest.xml裏,還有個metadata元素能夠配置。metadata是一個很是好的資源代碼解耦方式,在metadata裏配置的都是字符串,不論是否存在plugin,都不會影響app的編譯及運行,由於metadata的解析都在plugin端。
<meta-data
android:name="plugin-theme"
android:value="pluginDefaulTheme"/>
複製代碼
而後,咱們咱們就能夠得出構建plugin主題的方案了:
即,這樣咱們就能夠構造統一風格的plugin了。 具體的實現代碼以下:
PluginResLoader:
public Resources.Theme getTheme(Context context) {
if(context == null) {
return null;
}
Resources.Theme theme = context.getTheme();
String themeFromMetaData = null;
if(context instanceof Activity) {
Activity activity = (Activity)context;
try {
ActivityInfo info = activity.getPackageManager().getActivityInfo(activity.getComponentName(),
PackageManager.GET_META_DATA);
if(info != null && info.metaData != null) {
themeFromMetaData = info.metaData.getString(THEME_METADATA_NAME);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
} else {
//the context is not Activity,
//get metadata from Application.
try {
ApplicationInfo appInfo = context.getPackageManager()
.getApplicationInfo(context.getPackageName(),
PackageManager.GET_META_DATA);
if(appInfo != null && appInfo.metaData != null) {
themeFromMetaData = appInfo.metaData.getString(THEME_METADATA_NAME);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
if(themeFromMetaData == null) {
//get theme from metadata fail, return the theme from baseContext.
return theme;
}
int themeId = -1;
if(WIDGET_THEME_NAME.equals(themeFromMetaData)) {
themeId = getIdentifier(context, "style", WIDGET_THEME_NAME);
} else {
Log.w(TAG, "getTheme: the theme from metadata is wrong");
}
if(themeId >= 0) {
theme = getResources(context).newTheme();
theme.applyStyle(themeId, true);
}
return theme;
}
複製代碼