你們好,我叫小鑫,也能夠叫我蠟筆小鑫😊;java
本人17年畢業於中山大學,於2018年7月加入37手遊安卓團隊,曾經就任於久邦數碼擔任安卓開發工程師;android
目前是37手遊安卓團隊的海外負責人,負責相關業務開發;同時兼顧一些基礎建設相關工做微信
簡介 市面上實現插件化的方式大致可分爲兩種,一種是hook方式,一種是插樁式。其中hook方式,由於須要hook系統API,隨着系統API的變化須要不斷作適配。所以插樁式方案將來趨勢,我更看好代理方式實現的方案post
public interface IActivityInterface {
public void setAppContext(Activity activity);
public void onCreate(Bundle bundle);
public void setContentView(int layoutId);
}
複製代碼
public class BaseActivity implements IActivityInterface {
private Activity mActivity;
@Override
public void setAppContext(Activity activity) {
Log.i("我是插件", "setAppContext");
mActivity = activity;
}
@Override
public void onCreate(Bundle bundle) {
Log.i("我是插件", "onCreate");
}
@Override
public void setContentView(int layoutId) {
Log.i("我是插件", "setContentView");
mActivity.setContentView(layoutId);
}
}
複製代碼
public class PluMainActivity extends BaseActivity {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_plu);
}
}
複製代碼
Android中的ClassLoader類加載器派生出的有DexClassLoader和PathClassLoader。這二者的區別是測試
DexClassLoader: 可以加載未安裝的jar/apk/dexui
PathClassLoader: 只能加載系統中已經安裝的apkthis
同時,因爲虛擬機在安裝期間會爲類打上CLASS_ISPREVERIFIED標誌,當知足如下條件時:
在類加載時,因爲ClassLoader的雙親委託機制,加載時若是加載了插件中的類了,那麼宿主的類便不會再加載而會使用插件的,反之對插件也是同樣。這就很容易觸發上述所說的verify的問題,從而報出異常「java.lang.IllegalAccessError: Class ref in pre-verified class...」
如何避免?
能夠經過自定義ClassLoader修改類加載邏輯,使得插件和宿主中的類隔離,各自加載。
各自加載的好處:插件和宿主依賴的通用模塊無需特殊處理。
package com.sq.a37syplu10.plugin.loader;
import android.os.Build;
import dalvik.system.DexClassLoader;
public class ApkClassLoader extends DexClassLoader {
private ClassLoader mGrandParent;
private final String[] mInterfacePackageNames;
public ApkClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent, String[] interfacePackageNames) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
ClassLoader grand = parent;
mGrandParent = grand.getParent();
this.mInterfacePackageNames = interfacePackageNames;
}
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
String packageName;
int dot = className.lastIndexOf('.');
if (dot != -1) {
packageName = className.substring(0, dot);
} else {
packageName = "";
}
boolean isInterface = false;
for (String interfacePackageName : mInterfacePackageNames) {
if (packageName.equals(interfacePackageName)) {
isInterface = true;
break;
}
}
if (isInterface) {
return super.loadClass(className, resolve);
} else {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = mGrandParent.loadClass(className);
} catch (ClassNotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
e.addSuppressed(suppressed);
}
throw e;
}
}
}
return clazz;
}
}
/** * 從apk中讀取接口的實現 * * @param clazz 接口類 * @param className 實現類的類名 * @param <T> 接口類型 * @return 所需接口 * @throws Exception */
public <T> T getInterface(Class<T> clazz, String className) throws Exception {
try {
Class<?> interfaceImplementClass = loadClass(className);
Object interfaceImplement = interfaceImplementClass.newInstance();
return clazz.cast(interfaceImplement);
} catch (ClassNotFoundException | InstantiationException
| ClassCastException | IllegalAccessException e) {
throw new Exception(e);
}
}
}
複製代碼
上述代碼中,除了隔離宿主和插件的類加載外,還預留了白名單。由於宿主和插件中,遵循同一套標準時,就須要將插件中加載的類,轉爲宿主的標準的類型。根據同一個類加載器加載且全類名相同纔算同一個類,須要用父加載器加載的接口才能夠進行類型轉換。所以須要將IActivityInterface列入白名單。
同時,因爲插件中的類也存在verify的問題,BaseActivity引用了IActivityInterface,而且BaseActivity引用的類都屬於一個dex,BaseActivity會被打上標識。那麼當使用宿主的IActivityInterface時,就會 報錯。
那麼,怎麼解決?
將插件中的標準處理成jar包,使用compileOnly方式依賴,不打入插件apk中。這樣BaseActivity便不會被打上標識,問題解決。即宿主和插件中須要經過接口類型轉換的,將插件中該接口去除。
常規方案:
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, mPluginPath);
Resources resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
複製代碼
缺點1:使用了反射,而且addAssetPath方法已經廢棄,甚至在高版本中已經不存在該方法了
缺點2:只使用插件的Resouces,宿主的setContentView方法前的其餘資源加載不到,日誌中會有異常報出support包相關的資源找不到。
採用騰訊shadow中的方案:
第一步,加載插件中的resources,無需反射的方式以下:
private Resources buildPluginResources() {
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(
mContext.getPackageName(),
PackageManager.GET_ACTIVITIES
| PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES);
packageInfo.applicationInfo.publicSourceDir = mPluginPath;
packageInfo.applicationInfo.sourceDir = mPluginPath;
return mContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
複製代碼
第二步,利用宿主包的Resouces和插件包的Resouces混合出一個新的Resources。獲取資源時,先搜索插件的Resouces,若是找不到,則從宿主Resouces中找,代碼以下:
package com.sq.a37syplu10.plugin.resources;
import android.annotation.TargetApi;
import android.content.res.AssetFileDescriptor;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Movie;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.TypedValue;
import java.io.InputStream;
/** * Resources資源先從插件獲取,若是獲取不到則從宿主獲取 */
public class MixResources extends ResourcesWrapper {
private Resources mHostResources;
public MixResources(Resources hostResources, Resources pluginResources) {
super(pluginResources);
mHostResources = hostResources;
}
@Override
public CharSequence getText(int id) throws NotFoundException {
try {
return super.getText(id);
} catch (NotFoundException e) {
return mHostResources.getText(id);
}
}
@Override
public String getString(int id) throws NotFoundException {
try {
return super.getString(id);
} catch (NotFoundException e) {
return mHostResources.getString(id);
}
}
@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return super.getString(id,formatArgs);
} catch (NotFoundException e) {
return mHostResources.getString(id,formatArgs);
}
}
@Override
public float getDimension(int id) throws NotFoundException {
try {
return super.getDimension(id);
} catch (NotFoundException e) {
return mHostResources.getDimension(id);
}
}
@Override
public int getDimensionPixelOffset(int id) throws NotFoundException {
try {
return super.getDimensionPixelOffset(id);
} catch (NotFoundException e) {
return mHostResources.getDimensionPixelOffset(id);
}
}
@Override
public int getDimensionPixelSize(int id) throws NotFoundException {
try {
return super.getDimensionPixelSize(id);
} catch (NotFoundException e) {
return mHostResources.getDimensionPixelSize(id);
}
}
@Override
public Drawable getDrawable(int id) throws NotFoundException {
try {
return super.getDrawable(id);
} catch (NotFoundException e) {
return mHostResources.getDrawable(id);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawable(int id, Theme theme) throws NotFoundException {
try {
return super.getDrawable(id, theme);
} catch (NotFoundException e) {
return mHostResources.getDrawable(id,theme);
}
}
@Override
public Drawable getDrawableForDensity(int id, int density) throws NotFoundException {
try {
return super.getDrawableForDensity(id, density);
} catch (NotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return mHostResources.getDrawableForDensity(id, density);
} else {
return null;
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawableForDensity(int id, int density, Theme theme) {
try {
return super.getDrawableForDensity(id, density, theme);
} catch (Exception e) {
return mHostResources.getDrawableForDensity(id,density,theme);
}
}
@Override
public int getColor(int id) throws NotFoundException {
try {
return super.getColor(id);
} catch (NotFoundException e) {
return mHostResources.getColor(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public int getColor(int id, Theme theme) throws NotFoundException {
try {
return super.getColor(id,theme);
} catch (NotFoundException e) {
return mHostResources.getColor(id,theme);
}
}
@Override
public ColorStateList getColorStateList(int id) throws NotFoundException {
try {
return super.getColorStateList(id);
} catch (NotFoundException e) {
return mHostResources.getColorStateList(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {
try {
return super.getColorStateList(id,theme);
} catch (NotFoundException e) {
return mHostResources.getColorStateList(id,theme);
}
}
@Override
public boolean getBoolean(int id) throws NotFoundException {
try {
return super.getBoolean(id);
} catch (NotFoundException e) {
return mHostResources.getBoolean(id);
}
}
@Override
public XmlResourceParser getLayout(int id) throws NotFoundException {
try {
return super.getLayout(id);
} catch (NotFoundException e) {
return mHostResources.getLayout(id);
}
}
@Override
public String getResourceName(int resid) throws NotFoundException {
try {
return super.getResourceName(resid);
} catch (NotFoundException e) {
return mHostResources.getResourceName(resid);
}
}
@Override
public int getInteger(int id) throws NotFoundException {
try {
return super.getInteger(id);
} catch (NotFoundException e) {
return mHostResources.getInteger(id);
}
}
@Override
public CharSequence getText(int id, CharSequence def) {
try {
return super.getText(id,def);
} catch (NotFoundException e) {
return mHostResources.getText(id,def);
}
}
@Override
public InputStream openRawResource(int id) throws NotFoundException {
try {
return super.openRawResource(id);
} catch (NotFoundException e) {
return mHostResources.openRawResource(id);
}
}
@Override
public XmlResourceParser getXml(int id) throws NotFoundException {
try {
return super.getXml(id);
} catch (NotFoundException e) {
return mHostResources.getXml(id);
}
}
@TargetApi(Build.VERSION_CODES.O)
@Override
public Typeface getFont(int id) throws NotFoundException {
try {
return super.getFont(id);
} catch (NotFoundException e) {
return mHostResources.getFont(id);
}
}
@Override
public Movie getMovie(int id) throws NotFoundException {
try {
return super.getMovie(id);
} catch (NotFoundException e) {
return mHostResources.getMovie(id);
}
}
@Override
public XmlResourceParser getAnimation(int id) throws NotFoundException {
try {
return super.getAnimation(id);
} catch (NotFoundException e) {
return mHostResources.getAnimation(id);
}
}
@Override
public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
try {
return super.openRawResource(id,value);
} catch (NotFoundException e) {
return mHostResources.openRawResource(id,value);
}
}
@Override
public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
try {
return super.openRawResourceFd(id);
} catch (NotFoundException e) {
return mHostResources.openRawResourceFd(id);
}
}
}
複製代碼
package com.sq.a37syplu10.plugin;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import com.sq.a37syplu10.MainActivity;
import com.sq.a37syplu10.plugin.loader.ApkClassLoader;
import com.sq.aninterface.IActivityInterface;
public class ProxyPluginActivity extends Activity {
@Override
public ApkClassLoader getClassLoader() {
return MainActivity.mPlugin.mClassLoader;
}
@Override
public Resources getResources() {
return MainActivity.mPlugin.mResource;
}
private IActivityInterface pluginActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null && !TextUtils.isEmpty(intent.getStringExtra("activity"))) {
try {
pluginActivity = getClassLoader().getInterface(IActivityInterface.class, intent.getStringExtra("activity"));
pluginActivity.setAppContext(this);
pluginActivity.onCreate(new Bundle());
} catch (Exception e) {
e.printStackTrace();
}
} else {
Log.e("我是宿主", "intent 中沒帶插件activity信息");
}
}
@Override
public void startActivity(Intent intent) {
if (!TextUtils.isEmpty(intent.getStringExtra("activity"))) {
intent.setClass(this, ProxyPluginActivity.class);
}
super.startActivity(intent);
}
}
複製代碼
經測試,模擬器,真機從android4-10都正常。暫無遇到兼容問題
過程當中有問題或者須要交流的同窗,能夠掃描二維碼加好友,而後進羣進行問題和技術的交流等;