你们好,我叫小鑫,也能够叫我蜡笔小鑫😊;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都正常。暂无遇到兼容问题
过程当中有问题或者须要交流的同窗,能够扫描二维码加好友,而后进群进行问题和技术的交流等;