大家好,我叫小鑫,也可以叫我蜡笔小鑫😊;
本人17年毕业于中山大学,于2018年7月加入37手游安卓团队,曾经就职于久邦数码担任安卓开发工程师;
目前是37手游安卓团队的海外负责人,负责相关业务开发;同时兼顾一些基础建设相关工作
市面上实现插件化的方式大体可分为两种,一种是hook方式,一种是插桩式。其中hook方式,因为需要hook系统API,随着系统API的变化需要不断做适配。因此插桩式方案未来趋势,我更看好代理方式实现的方案
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/dex
PathClassLoader: 只能加载系统中已经安装的apk
同时,由于虚拟机在安装期间会为类打上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便不会被打上标识,问题解决。即宿主和插件中需要通过接口类型转换的,将插件中该接口去除。
缺点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都正常。暂无遇到兼容问题
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。