插件化?好像也就那么回事~

作者:高级攻城狮
转载地址:https://juejin.cn/post/7143869920193822733

在Android 系统中的应用都是以APK的形式存在,需要通过安装才能进行使用。这个大家都应该知道,但是实际上 Android系统安装应用的方式很简单,就是把应用APK 拷贝到系统的不同目录下,把so 解压出来。

那么Android 怎么运行应用中的代码呢?

这就得看APK的构成了,常见的APK一般会包含以下几个部分:

  • classes.dex:Java 代码字节码
  • res:资源文件
  • lib:so 文件
  • assets:静态资产文件
  • AndroidManifest.xml:清单文件

Android 系统在打开应用程序后,只是开辟了进程,用ClassLoader 加载 classes.dex 至进程中,执行对应的组件。

那为什么不能执行一个APK中的代码? 如果你有了解过插件化的概念就只其中一二了。

什么是插件化?

插件化就是将一个大的APK拆分程多个小APK的一个过程,而每个插件就是一个小的APK,编译生成插件apk叫插件工程。

插件化结构图:

插件化框架中的apk一般都有自己的插件ClassLoader,插件AssetManager,插件Context
而最底层的插件框架类似于我们的Android Framework层
插件化优缺点:

优点

1.较小apk的体积,可以根据需要下载对应的插件模块
2.插件可以单独作为apk进行调试,且互相解耦,可以多模块同时开发,提升开发效率
3.可以动态更新插件或插件补丁

缺点

1.对于已经成型的项目重构成本较大
2.很多插件化框架做不到对所有版本兼容。

插件化和组件化区别

  • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),
    开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。
  • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),
    最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。

1.插件类加载:

这里我们首先得了解下类加载的双亲委派原则

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去加载, 每一个层次的加载器都是如此, 因此所有的加载请求最终都会传送到最底层的启动类加载器, 只有父加载器无法完成加载的时候才会将加载任务向下传递个子类进行。

Android中的ClassLoader类关系

由于双亲委派原则存在,其加载过程如下

这里我们主要来看PathClassLoaderDexClassLoader这两个类加载器是我们Android中最重要的类加载器 查看源码:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath,String optimizedDirectory,String librarySearchPath,ClassLoader parent) {
        super((String)null,(File)null,(String)null,(ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

DexClassLoader类中就只有一个构造方法,构造方法中直接调用了父类的构造,DexClassLoader继承了BaseDexClassLoader,构造方法中的参数的含义是:

  • dexPath:dex文件路径
  • optimizedDirectory:dex文件首次加载时会进行优化操作,这个参数即为优化后的odex文件的存放目录,官方推荐使用应用私有目录来缓存优化后的dex文件,dexOutputDir = context.getDir(“dex”,0);
  • librarySearchPath:动态库路径
  • parent:当前类加载器的父类加载器

继续看PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath,(ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath,(ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

PathClassLoader有两个构造方法,同样也是直接调用了父类的构造方法,从构造方法上来看,DexClassLoader和PathClassLoader的区别只有第二个参数optimizedDirectory,在PathClassLoader中optimizedDirectory默认传入的是null。

从源码中看这两个类的作用也是因为optimizedDirectory参数的不同而不同在源码中看使用PathClassLoader由于没有传入optimizedDirectory,系统会自动生成以后缓存目录,即/data/dalvik-cache/,在这个目录存放优化以后的dex文件

所以PathClassLoader只能加载已安装的apk的dex,即加载系统的类和已经安装的应用程序(安装的apk的dex文件会存储在/data/dalvik-cache中),而DexClassLoader可以加载指定路径的apk、dex,也可以从sd卡中进行加载

基于上面的分析,在做插件化改造过程中只要创建一个DexClassLoader 对象,然后使用这个对象去加载外部路径的class文件即可: 简化过程如下:

private void loadClass() {
	init();
	try {
        // 从优化后的dex文件中加载APK_HELLO_CLASS_PATH类
        clazz = pluginClassLoader.loadClass("com.iflytek.test.HelloWorld");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}
private fun init() {
	extractPlugin()
	pluginPath = File(filesDir.absolutePath,"plugin.apk").absolutePath
	nativeLibDir = File(filesDir,"pluginlib").absolutePath
	dexOutPath = File(filesDir,"dexout").absolutePath
	// 生成 DexClassLoader 用来加载插件类
	pluginClassLoader = DexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class.java.classLoader)
}

// 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin() {
	var inputStream = assets.open("plugin.apk")
	File(filesDir.absolutePath,"plugin.apk").writeBytes(inputStream.readBytes())
}

获取到插件中的类加载器后,就可以通过反射的方式去调用插件中类的方法:

val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("hello",null).invoke(loadClass)

最后在需要加载插件中的类时:需要根据插件名称获取对应的插件ClassLoader即可

简化代码如下:

PluginClassLoader.load(“插件名”,"需要加载的插件类权限定名")

2.插件资源加载:

资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念, 所有资源(layout、values 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id。

资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口

PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的 PackageInfo
PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例

我们要做的就是在加载插件 Apk中的资源之前创建一个插件资源实例。 具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo, 有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:

PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

Resources injectResources = null;
try {
    injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
    // ...
}

拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:

public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources,Resources injectResources) {
        super(injectResources.getAssets(),injectResources.getDisplayMetrics(),injectResources.getConfiguration());
        this.hostResources = hostResources;
        this.injectResources = injectResources;
    }

    @Override
    public String getString(int id,Object... formatArgs) throws NotFoundException {
        try {
            return injectResources.getString(id,formatArgs);
        } catch (NotFoundException e) {
            return hostResources.getString(id,formatArgs);
        }
    }

    // ...
}

当然你也可以也可以把插件资源独立出来,第一次使用时通过插件名去对应路径下寻找apk,然后根据apk路径创建一个插件的PluginResources对象 最后缓存在内存中,下次就不用再重新创建插件资源

结合ClassLoader和资源加载过程,我们可以使用一个Context来包裹住插件中的这些对象:

public class PluginContext extends ContextThemeWrapper {
	//插件ClassLoader
	private final ClassLoader pluginClassLoader;
	//插件Resources
    private final Resources pluginResource;
	//插件名称,一般根据插件的apk名称来
    private final String mPlugin;
	...

	@Override
    public ClassLoader getClassLoader() {
        if (pluginClassLoader != null) {
            return pluginClassLoader;
        }
        return super.getClassLoader();
    }

	@Override
    public Resources getResources() {
        if (pluginResource != null) {
            return pluginResource;
        }
        return super.getResources();
    }

	@Override
    public AssetManager getAssets() {
        if (pluginResource != null) {
            return pluginResource.getAssets();
        }
        return super.getAssets();
    }
}

我们在使用插件中类和资源的时候,就可以通过PluginContext来获取ClassLoader和Resources, 得到插件中的类和资源

3.插件中四大组件通讯

在讲解插件中四大组件通讯前我们先来了解下Activity的启动过程

Activity的启动过程主要分为两种,一种是根Activity的启动过程,一种是普通Activity的启动过程。关于根Activity的启动过程在前面文章介绍过,这里来简单回顾下,如下图所示。

首先Launcher进程向AMS请求创建根Activity,AMS会判断根Activity所需的应用程序进程是否存在并启动,如果不存在就会请求Zygote进程创建应用程序进程。应用程序进程启动后,AMS会请求应用程序进程创建并启动根Activity。

普通Activity和根Activity的启动过程大同小异,但是没有这么复杂,因为不涉及应用程序进程的创建,跟Laucher也没关系,如下图所示。

上图抽象的给出了普通Acticity的启动过程。在应用程序进程中的Activity向AMS请求创建普通Activity(步骤1),AMS会对 这个Activty的生命周期管和栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动普通Activity.

可以看出:

我们需要启动一个Activity,就需要经过AMS校验,AMS会检测当前Activity是否在AndroidManifest.xml中注册过,如果没有注册就会报ActivityNotFoundException

下面给出方案:

  • 1.需要在AndroidManifest.xml中注册Activity进行占坑,使用占坑的方式骗过AMS的校验
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.music.anna.pluginactivity">S
    <application
       ...
        <activity android:name=".StubActivity"/>
    </application>
</manifest>
  • 2.在AMS返回到宿主工程后,还原需要启动的插件Activity,然后创建对应的Activity实例。

上面的方案有三种实现方式:

方式1:Hook Instrumentation

public class Instrumentation {
    //启动Activity的时候,调用此方法时,替换调Intent
    public ActivityResult execStartActivity(
            Context who,IBinder contextThread,IBinder token,Activity target,Intent intent,int requestCode,Bundle options) {

    }

    //AMS检测后,创建Activity之前替换回Intent
    public Activity newActivity(ClassLoader cl,String className,Intent intent)
            throws InstantiationException,IllegalAccessException,ClassNotFoundException {

    }
}

Instrumentation代理类如下:

public class InstrumentationProxy extends Instrumentation {
    private Instrumentation mInstrumentation;
    private PackageManager mPackageManager;
    public InstrumentationProxy(Instrumentation instrumentation,PackageManager packageManager) {
        mInstrumentation = instrumentation;
        mPackageManager = packageManager;
    }
	 //启动Activity的时候,调用此方法时,替换调Intent
    public ActivityResult execStartActivity(
            Context who,Bundle options) {
        List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent,PackageManager.MATCH_ALL);
        if (infos == null || infos.size() == 0) {
            intent.putExtra(HookHelper.TARGET_INTENsT_NAME,intent.getComponent().getClassName());//1
            intent.setClassName(who,"com.music.anna.pluginactivity.StubActivity");//2
        }
        try {
            Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",Context.class,IBinder.class,Activity.class,Intent.class,int.class,Bundle.class);
            return (ActivityResult) execMethod.invoke(mInstrumentation,who,contextThread,token,target,intent,requestCode,options);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

	//AMS检测后,创建Activity之前替换回Intent
	public Activity newActivity(ClassLoader cl,Intent intent) throws InstantiationException,ClassNotFoundException {
		String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
		if (!TextUtils.isEmpty(intentName)) {
			return super.newActivity(cl,intentName,intent);
		}
		return super.newActivity(cl,className,intent);
	}
}

最后使用HookHelper工具类将InstrumentationProxy替换系统中的mInstrumentation 代码如下:

public static void hookInstrumentation(Context context) throws Exception {
	Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
	Field mMainThreadField  =FieldUtil.getField(contextImplClass,"mMainThread");//1
	Object activityThread = mMainThreadField.get(context);//2
	Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
	Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");//3
	FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),context.getPackageManager()));
}

方式2:Hook IActivityManager.startActivity和ActivityThread.mH.mCallback

  • IActivityManager:用于应用进程和AMS进行通讯的binder对象,在调用AMS校验Activity前使用占坑Activity骗过AMS
  • ActivityThread.mH.mCallback:用于处理AMS校验后,返回到宿主的ApplicationThread线程中,处理Activity创建请求。 ActivityThread会通过H将代码的逻辑切换到主线程中,H类是ActivityThread的内部类并继承自Handler,如下所示。
frameworks/base/core/java/android/app/ActivityThread.java
private class H extends Handler {
public static final int LAUNCH_ACTIVITY         = 100;
public static final int PAUSE_ACTIVITY          = 101;
...
public void handleMessage(Message msg) {
    if (DEBUG_MESSAGES) Slog.v(TAG,">>> handling: " + codeToString(msg.what));
    switch (msg.what) {
        case LAUNCH_ACTIVITY: {
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,"activityStart");
            final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

            r.packageInfo = getPackageInfoNoCheck(
                    r.activityInfo.applicationInfo,r.compatInfo);
            handleLaunchActivity(r,null,"LAUNCH_ACTIVITY");
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        } break;
        ...
      }
...
}

H中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。那么在哪进行替换呢?接着来看Handler的dispatchMessage方法:

frameworks/base/core/java/android/os/Handler.java
public void dispatchMessage(Message msg) {
	if (msg.callback != null) {
		handleCallback(msg);
	} else {
		if (mCallback != null) {
			if (mCallback.handleMessage(msg)) {
				return;
			}
		}
		handleMessage(msg);
	}
}

Handler的dispatchMessage用于处理消息,看到如果Handler的Callback类型的mCallback不为null,就会执行mCallback的handleMessage方法。因此,mCallback可以作为Hook点,我们可以用自定义的Callback来替换mCallback,自定义的Callback如下所示。

HCallback.java

public class HCallback implements Handler.Callback{
    public static final int LAUNCH_ACTIVITY = 100;
    Handler mHandler;
    public HCallback(Handler handler) {
        mHandler = handler;
    }
    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            Object r = msg.obj;
            try {
                //得到消息中的Intent(启动SubActivity的Intent)
                Intent intent = (Intent) FieldUtil.getField(r.getClass(),r,"intent");
                //得到此前保存起来的Intent(启动TargetActivity的Intent)
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                //将启动SubActivity的Intent替换为启动TargetActivity的Intent
                intent.setComponent(target.getComponent());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

HCallback实现了Handler.Callback,并重写了handleMessage方法,当收到消息的类型为LAUNCH_ACTIVITY时,将启动SubActivity的Intent替换为启动TargetActivity的Intent。接着我们在HookHelper中定义一个hookHandler方法如下所示。

public static void hookHandler() throws Exception {
	Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
	Object currentActivityThread= FieldUtil.getField(activityThreadClass,"sCurrentActivityThread");//1
	Field mHField = FieldUtil.getField(activityThread,"mH");//2
	Handler mH = (Handler) mHField.get(currentActivityThread);//3
	FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
}

ActivityThread类中有一个静态变量sCurrentActivityThread,用于表示当前的ActivityThread对象, 因此

  • 注释1处获取ActivityThread中定义的sCurrentActivityThread对象。
  • 注释2处获取ActivityThread类的mH字段,
  • 接着在注释3处获取当前ActivityThread对象中的mH对象,
  • 最后用HCallback来替换mH中的mCallback。

在MyApplication的attachBaseContext方法中调用HookHelper的hookHandler方法,运行程序,当我们点击启动插件按钮,发现启动的是插件TargetActivity。

方式3:Hook住ClassLoader

这里我们使用RePlugin框架的原理来讲解下: RePlugin全局只hook了一个点,那就是ClassLoader 原理图:

从原理图中我们看到Replugin hook了两个ClassLoader

  • RePluginClassLoader:继承PathClassLoader所以只能用于加载宿主中的已经安装的类。
  • PluginDexClassLoader:继承自DexClassLoader,前面我们分析过,DexClassLoader可以加载外部路径上的类

步骤2中:找到对应的插件中四大组件信息

插件信息使用下面方式获取:

  • 2.1:获取插件的mPackageInfo
 mPackageInfo = pm.getPackageArchiveInfo(mPath,PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_META_DATA);
  • 2.2:通过mPackageInfo获取四大组件信息以及配置清单文件信息
/**
 * 初始化ComponentList对象 <p>
 * 注意:仅框架内部使用
 */
public ComponentList(PackageInfo pi,String path,PluginInfo pli) {
    if (pi.activities != null) {
        for (ActivityInfo ai : pi.activities) {
            if (LOG) {
                    LogDebug.d(PLUGIN_TAG,"activity=" + ai.name);
            }
            ai.applicationInfo.sourceDir = path;
            // todo extract to function
            if (ai.processName == null) {
                    ai.processName = ai.applicationInfo.processName;
            }
            if (ai.processName == null) {
                    ai.processName = ai.packageName;
            }
            mActivities.put(ai.name,ai);
        }
    }
    if (pi.providers != null) {
        for (ProviderInfo ppi : pi.providers) {
            if (LOG) {
                    LogDebug.d(PLUGIN_TAG,"provider=" + ppi.name + "; auth=" + ppi.authority);
            }
            if (ppi.processName == null) {
                    ppi.processName = ppi.applicationInfo.processName;
            }
            if (ppi.processName == null) {
                    ppi.processName = ppi.packageName;
            }
            mProvidersByName.put(ppi.name,ppi);
            mProvidersByAuthority.put(ppi.authority,ppi);
        }
    }
    if (pi.services != null) {
        for (ServiceInfo si : pi.services) {
            if (LOG) {
                    LogDebug.d(PLUGIN_TAG,"service=" + si.name);
            }
            if (si.processName == null) {
                    si.processName = si.applicationInfo.processName;
            }
            if (si.processName == null) {
                    si.processName = si.packageName;
            }
            mServices.put(si.name,si);
        }
    }
    if (pi.receivers != null) {
        for (ActivityInfo ri : pi.receivers) {
            if (LOG) {
                    LogDebug.d(PLUGIN_TAG,"receiver=" + ri.name);
            }
            if (ri.processName == null) {
                    ri.processName = ri.applicationInfo.processName;
            }
            if (ri.processName == null) {
                    ri.processName = ri.packageName;
            }
            mReceivers.put(ri.name,ri);
        }
    }
    // 解析 Apk 中的 AndroidManifest.xml
    String manifest = getManifestFromApk(path);
}	

步骤3中:给插件中的Activity分配占位Activity,用于欺骗AMS

// 远程分配坑位
container = client.allocActivityContainer(plugin,process,ai.name,intent);

步骤8和9中:这两个步骤就是取出坑位Activity中真正要启动的插件Activity,通过映射获取

前面说过Activity在回调的时候会启动InstrumentationnewActivity创建Activity:

public Activity newActivity(ClassLoader cl,Intent intent)
		throws InstantiationException,ClassNotFoundException {
	String pkg = intent != null && intent.getComponent() != null
			? intent.getComponent().getPackageName() : null;
	return getFactory(pkg).instantiateActivity(cl,intent);
}
public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl,@NonNull String className,@Nullable Intent intent)
		throws InstantiationException,ClassNotFoundException {
	return (Activity) cl.loadClass(className).newInstance();
}

最终调用cl.loadClass(className).newInstance()创建一个实例对象:

这个时候的:

  • cl:还是宿主的PluginClassLoader,
  • className:占坑Activity类名

要实现启动插件Activity,我们就需要来看宿主的RePluginClassLoader的loadClass方法是如何操作的:

protected Class<?> loadClass(String className,boolean resolve) throws ClassNotFoundException {
    //
    Class<?> c = null;
    c = PMF.loadClass(className,resolve);
    if (c != null) {
        return c;
    }
    //
    try {
        c = mOrig.loadClass(className);
        // 只有开启“详细日志”才会输出,防止“刷屏”现象
        if (LogDebug.LOG && RePlugin.getConfig().isPrintDetailLog()) {
                LogDebug.d(TAG,"loadClass: load other class,cn=" + className);
        }
        return c;
    } catch (Throwable e) {
        //
    }
    //
    return super.loadClass(className,resolve);
}

进入PMF.loadClass(className,resolve);

final Class<?> loadClass(String className,boolean resolve) {
    // 加载Service中介坑位
    if (className.startsWith(PluginPitService.class.getName())) {
        return PluginPitService.class;
    }

    //
    if (mContainerActivities.contains(className)) {
        Class<?> c = mClient.resolveActivityClass(className);
        if (c != null) {
                return c;
        }
        return DummyActivity.class;
    }

    //
    if (mContainerServices.contains(className)) {
        Class<?> c = loadServiceClass(className);
        if (c != null) {
                return c;
        }
        return DummyService.class;
    }

    //
    if (mContainerProviders.contains(className)) {
        Class<?> c = loadProviderClass(className);
        if (c != null) {
                return c;
        }
        return DummyProvider.class;
    }
    ...
    return loadDefaultClass(className);
}

可以看到这里面获取的是映射表中的插件四大组件class类,返回的是插件中的类

这里class类就是通过前面说的插件ClassLoader:PluginDexClassLoader进行加载。

PluginDexClassLoader中重写了loadClass方法,我们就入源码看看:

@Override
protected Class<?> loadClass(String className,boolean resolve) throws ClassNotFoundException {
    // 插件自己的Class。从自己开始一直到BootClassLoader,采用正常的双亲委派模型流程,读到了就直接返回
    Class<?> pc = null;
    ClassNotFoundException cnfException = null;
    try {
        pc = super.loadClass(className,resolve);
        if (pc != null) {
                ...
                return pc;
        }
    } catch (ClassNotFoundException e) {
        if (PluginDexClassLoaderPatch.need2LoadFromHost(className)) {
                try {
                        return loadClassFromHost(className,resolve);
                } catch (ClassNotFoundException e1) {

                }
        }
    }

    // 若插件里没有此类,则会从宿主ClassLoader中找,找到了则直接返回
    // 注意:需要读取isUseHostClassIfNotFound开关。默认为关闭的。可参见该开关的说明
    if (RePlugin.getConfig().isUseHostClassIfNotFound()) {
        try {
                return loadClassFromHost(className,resolve);
        } catch (ClassNotFoundException e) {

        }
    }
    // At this point we can throw the previous exception
    if (cnfException != null) {
        throw cnfException;
    }
    return null;
}

可以看到其在加载类的时候,会优先加载当前插件中的类,如果没找到,再去加载宿主中的类 来看。这样就可以成功找到插件中的Activity,之后就可以跳转了。

这里借鉴下恋猫de小郭的图来描述下Replugin中的ClassLoader调用关系:

通过这几个步骤及实现了只hook ClassLoader实现插件和宿主之间的通讯。

4.运行时容器技术(ProxyActivity代理)

四大组件通讯除了上面的Activity占坑方式外,还可以使用一种运行时容器技术

运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。 它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:

  • pluginName
  • pluginApkPath
  • pluginActivityName

等,其实最重要的就是 pluginApkPathpluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:

  • 转发所有来自系统的生命周期回调至插件 Activity
  • 接受 Activity 方法的系统调用,并转发回系统

我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity

public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        String pluginActivityName = getIntent().getString("pluginActivityName","");
        pluginActivity = PluginLoader.loadActivity(pluginActivityName,this);
        if (pluginActivity == null) {
            super.onCreate(savedInstanceState);
            return;
        }

        pluginActivity.onCreate();
    }

    @Override
    protected void onResume() {
        if (pluginActivity == null) {
            super.onResume();
            return;
        }
        pluginActivity.onResume();
    }

    @Override
    protected void onPause() {
        if (pluginActivity == null) {
            super.onPause();
            return;
        }
        pluginActivity.onPause();
    }

    // ...
}

大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。

最后来介绍下几种主流插件化开源框架:

阿里系Atlas:github.com/alibaba/atl…

Atlas容器框架

支持特性

功能 说明
四大组件支持 支持运行bundle中的四大组件
共享代码资源 bundle可以直接使用host中的代码和资源
bundle按需加载 业务需要时,才会去加载对应bundle中的代码和资源
远程bundle 减少包体积。不常用的bundle放在云端,需要时按需下载。当用户设备空间紧张时,可以清理掉一些长期不用的组件
解释执行 为了降低用户等待时间,Atlas框架在dalivk系统上首次使用bundle时关闭了verify,在ART系统上首次使用时关闭了dex2oat走解释执行。同时后台通过异步任务走原生的dexopt过程,为下次使用做好准备

360系RePlugin:github.com/Qihoo360/Re…

RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案:

  • 完整的:让插件运行起来“像单品那样”,支持大部分特性
  • 稳定的:如此灵活完整的情况下,其框架崩溃率仅为业内很低的“万分之一”
  • 适合全面使用的:其目的是让应用内的“所有功能皆为插件”
  • 占坑类:以稳定为前提的Manifest占坑思路
  • 插件化方案:基于Android原生API和语言来开发,充分利用原生特性

支持特性

Feature Description
Components Activity,Service,Provider,Receiver(Including static)
Not need to upgrade when brand a new Plug-in Supported
Android Feature Supported almost all features
TaskAffinity & Multi-Process Perfect supported!
Support Plug-in Type Built-in (Only Two Step) and External(Download)
Plug-in Coupling Binder,Class Loader,Resources,etc.
Interprocess communication Sync,Async,Binder and Cross-plug-in broadcast
User-Defined Theme & AppComat Supported
DataBinding Supported
Safety check when installed Supported
Resources Solution Independent Resources + Context pass(No Adaptation ROM)
Android Version API Level 9 (Android 2.3 and above)

高中生罗迪VirtualApp:github.com/asLody/Virt…

VirtualApp 作者是高中生罗迪,据说这个 Android 大牛初三的时候就开始研究双开、插件化的技术,相当了不起。 项目的思路与DroidPlugin 相似,不过他没有提供 Service 的代理,而是使用 ContentProvider 来代替 Service 在宿主中作为真正的运行体。 这款框架在2017年12月份已经作废,不过商用版本在更新。

原理:hook了AMS

腾讯系Shadow:github.com/Tencent/Sha…

支持特性

  • 四大组件
  • Fragment(代码添加和Xml添加)
  • DataBinding(无需特别支持,但已验证可正常工作)
  • 跨进程使用插件Service
  • 自定义Theme
  • 插件访问宿主类
  • So加载
  • 分段加载插件(多Apk分别加载或多Apk以此依赖加载)
  • 一个Activity中加载多个Apk中的View 等等……

关于如何选择插件化框架,这个见仁见智,可以根据自身项目需求和框架特性进行选择。

总结

讲了那么多这里是该总结下了: 本文主要讲解了当前主流插件化使用到的插件化技术

主要有:
1.类的加载过程以及原理
2.资源的注入过程以及原理
3.插件和宿主之间四大组件通讯机制。说到了几种hook方式
4.介绍了几种主流框架的特性

一般大厂都有自己的开源框架,而开源出来的部分只是冰山一角,但我们也希望通过这一角来窥探道插件化内部的奥秘


最后分享一份我自己收录整理的Android学习PDF+架构视频+面试文档+核心笔记,高级架构技术进阶脑图、Android开发面试专题文档,高级进阶架构文档。这些都是我现在闲暇还会反复翻阅的精品学习文档。里面对近几年的大厂面试高频问题都有详细的讲解,也是对我这次面试通过有很大的帮助。相信可以有效的帮助大家掌握知识、理解原理。当然你也可以拿去查漏补缺,提升自身的竞争力。↓↓↓

有需要的可以复制下方链接,传送直达!!!
https://qr21.cn/CaZQLo?BIZ=ECOMMERCE

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具