这里只分析主线的源码
入口:MainWindow
GUI层级选择完文件之后便跳转到MainWindow的open方法中,打开文件分两种类型,一种是后缀为.jadx的,另一种是其它的类型,比如:apk、aar、jar等,其中apk是最复杂的。
private final transient JadxWrapper wrapper;
void open(Path path, Runnable onFinish) {
backgroundExecutor.execute(NLS.str("progress.load"),
() -> wrapper.openFile(path.toFile()),
() -> {
......
});
}
打开文件使用的是JadxWrapper类中的openFile方法,具体如下:
public void openFile(File file) {
close();
this.openFile = file;
try {
JadxArgs jadxArgs = settings.toJadxArgs();
jadxArgs.setInputFile(file);
this.decompiler = new JadxDecompiler(jadxArgs);
this.decompiler.load();
} catch (Exception e) {
LOG.error("Jadx init error", e);
close();
}
}
JadxArgs是一个实体类,里面除了存放输入和输出的文件、输出的文件夹、反编译线程池的大小等,存放;设置完输入的文件后,使用JadxDecompiler执行反编译操作,我们接着看下它的load方法:sfsad
private JadxPluginManager pluginManager = new JadxPluginManager();
public void load() {
......
loadInputFiles();
root = new RootNode(args);
root.loadClasses(loadedInputs);
root.initClassPath();
root.loadResources(getResources());
root.initPasses();
root.runPreDecompileStage();
}
private void loadInputFiles() {
loadedInputs.clear();
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
for (JadxInputPlugin inputPlugin : pluginManager.getInputPlugins()) {
loadedInputs.add(inputPlugin.loadFiles(inputPaths));
}
}
JadxDecompiler是一个非常核心的管理类,里面维护了一个反编译的线程池,用ConcurrentHashMap结构存储类、方法、变量的信息,Root节点信息,资源列表,java类的列表(这个和前面ConcurrentHashMap存储的方法有关联),利用多线程可以提高反编译的效率;
下面具体看下代码相关的,里面很重要的一个方法是loadInputFiles,默认的输入文件是1个,然后JadxPluginManager会根据项目中加载的插件去加载文件的内容。
JadxPluginManager负责加载和管理所有的JadxInputPlugin插件,这些插件是使用ServiceLoader进行加载的,JadxInputPlugin是一个接口,具体在jadx-plugins-api模块中,它属于jadx-plugins下的子模块,整个项目中继承JadxInputPlugin的类有两个,分别是:JavaConvertPlugin(在jadx-java-convert下)和DexInputPlugin(在jadx-dex-input下)。SericeLoader属于java SPI(Service Provider Interfaces)范畴,能够很好的实现服务提供和使用解耦。
然后具体看loadInputFiles方法,核心的点在于JadxPluginManager使用不同的plugin加载文件的内容,jadx中主要使用的就是前面讲到的两个插件:JavaConvertPlugin和DexInputPlugin
JavaConvertPlugin详解
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo("java-convert", "JavaConvert", "Convert .jar and .class files to dex");
}
根据getPluginInfo的第三个参数的描述,JavaConvertPlugin的主要功能是将.jar、.class转为dex
@Override
public ILoadResult loadFiles(List<Path> input) {
ConvertResult result = JavaConvertLoader.process(input);
if (result.isEmpty()) {
result.deleteTemp();
return EmptyLoadResult.INSTANCE;
}
List<DexReader> dexReaders = DexFileLoader.collectDexFiles(result.getConverted());
return new DexLoadResult(dexReaders) {
@Override
public void close() throws IOException {
super.close();
result.deleteTemp();
}
};
}
接下来具体看下JavaConvertPlugin的loadFiles方法,JavaConvertLoader会将文件处理一下,主要的功能是判断是否是.jar或者.class文件,如果文件是apk,result的结果为空。
DexInputPlugin详解
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo("dex-input", "DexInput", "Load .dex and .apk files");
}
DexInputPlugin的主要功能是加载.dex和apk文件
@Override
public ILoadResult loadFiles(List<Path> input) {
return new DexLoadResult(DexFileLoader.collectDexFiles(input));
}
DexInputPlugin的 loadFiles核心是用DexFileLoader加载的,DexLoadResult只不过是将结果进行了转换,接下来看DexFileLoader核心部分:
private static List<DexReader> loadDexFromPath(Path path, int depth) {
try (InputStream inputStream = Files.newInputStream(path, StandardOpenOption.READ)) {
byte[] magic = new byte[DexConsts.MAX_MAGIC_SIZE];
if (inputStream.read(magic) != magic.length) {
return Collections.emptyList();
}
if (isStartWithBytes(magic, DexConsts.DEX_FILE_MAGIC)) {
return Collections.singletonList(new DexReader(path));
}
if (depth == 0 && isStartWithBytes(magic, DexConsts.ZIP_FILE_MAGIC)) {
return collectDexFromZip(path, depth);
}
} catch (Exception e) {
LOG.error("File open error: {}", path, e);
}
return Collections.emptyList();
}
//Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1
这里使用读取4个字节的方式,来判断是不是我们需要的文件,然后根据文件hex头部判断是dex或者zip,其中dex的文件头魔法值为:0x64, 0x65, 0x78, 0x0a ,zip文件的文件头魔法值为:0x50, 0x4B, 0x03, 0x04,大家可以使用notepad++或者sublime的hex插件对比下这两种文件的文件头。这两种方式本质上都会执行DexReader的方法,所以我们这边直接看DexReader的内容。
public DexReader(Path path) throws IOException {
this.path = path;
this.buf = ByteBuffer.wrap(Files.readAllBytes(path));
this.header = new DexHeader(new SectionReader(this, 0));
}
DexReader只负责将dex的所有的内容读取到ByteBuffer中,然后使用SectionReader将buffer复制一份,DexHeader负责读取属性的大小和偏移量,下面是截取的一小部分:
public DexHeader(SectionReader buf) {
......
fieldIdsSize = buf.readInt();
fieldIdsOff = buf.readInt();
methodIdsSize = buf.readInt();
methodIdsOff = buf.readInt();
classDefsSize = buf.readInt();
classDefsOff = buf.readInt();
......
}
核心的部分在SectionReader中,里面负责了变量、方法、类等信息的读取。至此,DexInputPlugin加载dex的大体流程已经介绍完了。
这整个的流程只是将apk当做一个zip文件进行读取,但并没有涉及到具体的读取操作,比如怎样将一个类完整的读取出来。