我报名参与金石计划1期挑战——分割10万奖池,这是我的第2篇文章,点击检查活动详情

🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这儿有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不走失!

前语

想必咱们都知道,在 Android 体系中,运用是以 Apk 的形式存在的,运用都需求装置才干运用。
但实际上 Android 体系装置运用的办法适当简略,其实便是把运用 Apk 拷贝到体系不同的目录下、然后把 so 解压出来罢了

常见的运用装置目录有:

  • /system/app:体系运用
  • /system/priv-app:体系运用
  • /data/app:用户运用

那可能咱们会想问,已然装置这个进程如此简略,Android 是怎么运转运用中的代码的呢,咱们先看 Apk 的构成,一个常见的 Apk 会包括如下几个部分:

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

其实 Android 体系在打开运用之后,也只是拓荒进程,然后运用 ClassLoader 加载 classes.dex 至进程中,履行对应的组件罢了。

已然 Android 本身也是运用类似反射的形式加载代码履行,凭什么咱们不能履行一个 Apk 中的代码呢?
这就需求引进插件化相关概念:

插件化相关概念:

  • 1.插件:一个插件其实便是一个apk
  • 2.插件工程:能够编译生成插件apk的工程
  • 3.插件化:将一个大的apk拆分红多个小apk的进程

插件化结构图

插件结构.png
插件化结构中的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.插件类加载
  • 2.插件资源加载
  • 3.插件四大组件通讯
  • 4.插件动态布置

1.插件类加载:

这儿咱们首先得了解下类加载的双亲派遣准则

假如一个类加载器收到了类加载的恳求,它首先不会自己去测验加载这个类,而是把这个恳求派遣给父类加载器去加载,
每一个层次的加载器都是如此, 因而一切的加载恳求终究都会传送到最底层的发动类加载器,
只要父加载器无法完结加载的时分才会将加载任务向下传递个子类进行。

Android中的ClassLoader类联系

插件双亲派遣.webp

由于双亲派遣准则存在,其加载进程如下

插件化类加载进程.webp

这儿咱们主要来看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 parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (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的发动进程在前面文章介绍过,这儿来简略回顾下,如下图所示。
Activity发动进程.awebp

首先Launcher进程向AMS恳求创立根Activity,AMS会判断根Activity所需的运用程序进程是否存在并发动,假如不存在就会恳求Zygote进程创立运用程序进程。运用程序进程发动后,AMS会恳求运用程序进程创立并发动根Activity。

一般Activity和根Activity的发动进程大同小异,但是没有这么复杂,由于不涉及运用程序进程的创立,跟Laucher也没联系,如下图所示。

一般Activity发动进程.awebp

上图抽象的给出了一般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, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, 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, 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, String className, Intent intent) throws InstantiationException,
        IllegalAccessException, 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 ,null,"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插件原理图.webp
从原理图中咱们看到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, String className,
		Intent intent)
		throws InstantiationException, IllegalAccessException,
		ClassNotFoundException {
	String pkg = intent != null && intent.getComponent() != null
			? intent.getComponent().getPackageName() : null;
	return getFactory(pkg).instantiateActivity(cl, className, intent);
}
public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
		@Nullable Intent intent)
		throws InstantiationException, IllegalAccessException, 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调用联系:

双ClassLoader联系.awebp

经过这几个进程及完结了只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容器结构
atlas容器结构.png支撑特性

功用 阐明
四大组件支撑 支撑运转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插件化原理(一)Activity插件化

  • Atlas官方文档

  • 浅谈Android插件化

  • RePlugin原理介绍

  • 插件化的原理分析及完结