聊聊Android的静态代理插件框架原理[01]--Apk文件的加载

无论哪一种插件化框架,本质上都有宿主和插件这两个成员。而插件包通常都是以Apk文件的形式存在,所以如何加载Apk文件就是插件化基础中的基础了。本文就简单聊一下Android中如何加载apk文件吧。

Apk文件的加载

apk文件大家都不陌生,那Dex文件是什么呢?

Dex文件格式详解 引用文中的说明:

Dex文件是Android系统的可执行文件,包含应用程序的全部操作指令以及运行时数据.

由于dalvik是一种针对嵌入式设备而特殊设计的java虚拟机,所以dex文件与标准的class文件在结构设计上有着本质的区别

当java程序编译成class后,还需要使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,
在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右

Dalvik 可执行文件格式 这里是官方说明,具体解释了Dex文件的格式,这里就不多说了。

为什么这里提到Dex文件?因为在我们这套插件化框架中,『插件』本质上是一个Apk文件或者说Dex文件,插件化框架的App主动去加载这样的『Apk格式或者说Dex格式的插件』,从而实现所谓的插件化。具体表现形式例如:『打开一个没有在Manifest里面申明的Activity』,『加载插件中的资源』等等。

我们知道,java代码经过java虚拟机的编译后,生成的是class文件,那么我们是不是在Android系统中去加载class文件呢?

答案是不能的。原因如下:

在使用Java虚拟机时,我们经常自定义继承自ClassLoader的类加载器。
然后通过defineClass方法来从一个二进制流中加载Class。而在Android中我们无法这么使用,
Android中ClassLoader的defineClass方法具体是调用VMClassLoader的defineClass本地静态方法。
而这个本地方法什么都没做,只是抛出了一个“UnsupportedOperationException”异常。 
1
2
3
4
5
6
@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
throw new UnsupportedOperationException("can't load this type of class file");
}

在Android Framework层中,BaseDexClassLoader继承自ClassLoader类,并派生出两个类,分别是:DexClassLoader和PathClassLoader。我们来看一下官方文档是如何介绍这两个类的:

先看DexClassLoader

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.

This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getCodeCacheDir() to create such a directory:

File dexOutputDir = context.getCodeCacheDir();

Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks.

说的很清楚了,DexClassLoader可以用来加载包含classes.dex的jar文件或者apk文件中的类。这些类虽然安装的时候并不在应用中,但是加载后可以作为代码去执行。

还有一个比较重要的地方就是,DexClassLoader只能从应用私有的、可写的目录去缓存类。

然后再看PathClassLoader

Provides a simple ClassLoader implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).

简单来说,PathClassLoader只能加载系统中已经安装过的apk。

这里有一篇博客,DexClassLoader和PathClassLoader的区别,大家可以参考下,这里我就不多说了。接下来我们进入实战,看一下如何利用DexClassLoader去加载一个apk文件。

首先我们需要把apk移到应用的私有目录下:

1
2
3
String apkFileName = "demo.apk";
String dexOutputPath = context.getCodeCacheDir().getPath() + File.separator + apkFileName;
//复制apk文件到dexOutputPath路径

接着我们需要构建一个DexClassLoader类,

1
2
3
4
5
File dexOutputDir = mAppContext.getDir("dex", Context.MODE_PRIVATE);
String dexOptimizedPath = dexOutputDir.getAbsolutePath() + File.separator + apkFileName;
FileUtils.createDirIfNotExists(dexOptimizedPath);
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexOptimizedPath, null,
mAppContext.getClassLoader());

好了,有了DexClassLoader实例之后,我们使用反射的方式,加载我们需要的类和方法。比如说,我们可以在主app里定义一个接口

1
2
3
4
public interface MessageHandler {

void handleMessage(String message);
}

接着,我们在插件apk的代码里,定义MessageHandler的实现

1
2
3
4
5
6
7
public class PluginMessageHandler implements MessageHandler {

@Override
public void handleMessage(String message) {
Log.d(TAG, "receive the message " + message);
}
}

现在,我们可以去反射一下拿到MessageHandler的实例啦:

1
2
3
4
5
6
7
8
9
10
11
12
13
MessageHandler messageHandler;
try {
Class<?> localClass = dexClassLoader.loadClass("包名" + ".PluginMessageHandler");
Constructor<?> localConstructor = localClass.getConstructor();
Object instance = localConstructor.newInstance();
messageHandler = (PluginMessageHandler) instance;
} catch (Exception e) {
e.printStackTrace();
}

if(messageHandler != null) {
messageHandler.handleMessage("This is a message~");
}

日志打印:

receive the message This is a message~

本篇小结

现在我们知道了如何加载一个apk插件包并调用插件内的代码。接下来我们面临两个严峻的问题:

  1. Android中的四大组件,例如Activity, Service, ContentProvider,必须通过Manifest里进行静态注册,单纯通过反射的方式拿到这些类,并不能真正的在Android系统中运行这四大组件。

  2. 其次是资源加载的问题,这个也是一个很大的课题,我会在后续的文章中和大家一起探讨。