怎样修改火影战记的AndroidManifest.xml和classes.dex 反编译的内容?我一改就会闪退。

从Instant run谈Android替换Application和动态加载机制 - 推酷
从Instant run谈Android替换Application和动态加载机制
Android studio 2.0 Stable
版本中集成了
Install run
即时编译技术,官方描述可以大幅加速编译速度,我们团队在第一时间更新并使用,总体用下来感觉,恩…也就那样吧,还不如不用的快。所以就去看了下
Install run
的实现方式,其中有一个整体框架的基础,也就是今天的文章的主题,Android替换Application和动态加载机制。
Instant run
Instant run
的大概实现原理可以看下这篇
,我们需要知道
Instant run
gradle plugin2.0.0
,文中大概讲了下
Instant run
的实现原理,但是并没有深入细节,特别是替换Application和动态加载机制。
关于动态加载,实际上
Instant run
提供了两种动态加载的机制:
1.修改java代码需要重启应用加载补丁dex,而在Application初始化时替换了Application,新建了一个自定义的ClassLoader去加载所有的dex文件。我们称为
重启更新机制
2.修改代码不需要重启,新建一个
ClassLoader
去加载修改部分。我们称为
热更新机制
Application入口
Instant run
Transform API
修改字节码文件。其中
AndroidManifest.xml
文件也被修改,如下:
/app/build/intermediates/bundles/production/instant-run/AndroidManifest.xml
Application
&application
name=&com.aa.bb.MyApplication&
android:name=&com.android.tools.fd.runtime.BootstrapApplication&
com.android.tools.fd.runtime.BootstrapApplication
,在刚刚提到的
gradle plugin
instant-run-server
目录下找到该文件。
BootstrapApplication
是我们app的实际入口,我们自己的
Application
MyApplication
采用反射机制调用。
Application
ContextWrapper
// android.app.Applicationpublic class Application extends ContextWrapper {
public application() {
super(null);
// ...}// android.content.ContextWrapperpublic class ContextWrapper extends Context {
Context mB
public ContextWrapper(Context base) {
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException(&Base context already set&);
public AssetManager getAssets() {
return mBase.getAssets();
public Resources getResources()
return mBase.getResources();
ContextWrapper一方面继承了Context,一方面又包含(composite)了一个Context对象(称为mBase),对Context的实现为转发给mBase对象处理。上面的代码表示,在
attachBaseContext
方式调用之前Application是没有用的,因为mBase是空的。所以我们看下
BootstrapApplication
attachBaseContext
protected void attachBaseContext(Context context) {
if (!AppInfo.usingApkSplits) {
createResources(apkModified);
//新建一个ClassLoader并设置为原ClassLoader的parent
setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
//通过Manifest中我们的实际Application即MyApplication名反射生成对象
createRealApplication();
//调用attachBaseContext完成初始化
super.attachBaseContext(context);
if (realApplication != null) {
//反射调用实际Application的attachBaseContext方法
Method attachBaseContext =
ContextWrapper.class.getDeclaredMethod(&attachBaseContext&, Context.class);
attachBaseContext.setAccessible(true);
attachBaseContext.invoke(realApplication, context);
} catch (Exception e) {
throw new IllegalStateException(e);
初始化ClassLoader
//BootstrapApplication.setupClassLoadersprivate static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) {
// /data/data/package_name/files/instant-run/dex/目录下的dex列表
List&String& dexList = FileManager.getDexList(context, apkModified);
ClassLoader classLoader = BootstrapApplication.class.getClassLoader();
String nativeLibraryPath = (String) classLoader.getClass().getMethod(&getLdLibraryPath&)
.invoke(classLoader);
IncrementalClassLoader.inject(
classLoader,
nativeLibraryPath,
codeCacheDir,
//IncrementalClassLoader.injectpublic static ClassLoader inject(
ClassLoader classLoader, String nativeLibraryPath, String codeCacheDir,
List&String& dexes) {
//新建一个自定义ClassLoader,dexPath为参数中的dexList
IncrementalClassLoader incrementalClassLoader =
new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes);
//设置为原ClassLoader的parent
setParent(classLoader, incrementalClassLoader);
return incrementalClassL
新建一个自定义的
ClassLoader
名为IncrementalClassLoader,该
ClassLoader
很简单,就是
BaseDexClassLoader
的一个子类,并且将
IncrementalClassLoader
设置为原ClassLoader的parent,熟悉JVM加载机制的同学应该都知道,由于ClassLoader采用双亲委托模式,即委托父类加载类,父类找不到再自己去找。这样
IncrementalClassLoader
就变成了整个App的所有类的加载的ClassLoader,并且dexPath是
/data/data/package_name/files/instant-run/dex
目录下的dex列表,这意味着什么呢?
//``BaseDexClassLoader``的``findClass``protected Class&?& findClass(String name) throws ClassNotFoundException {
List&Throwable& suppressedExceptions = new ArrayList&Throwable&();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(&Didn't find class \&& + name + &\& on path: & + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
可以看到,查找Class的任务通过pathList完成;这个pathList是一个DexPathList类的对象,它的findClass方法如下:
public Class findClass(String name, List&Throwable& suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexF
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
return null;}
这个DexPathList内部有一个叫做dexElements的数组,然后findClass的时候会遍历这个数组来查找Class。看到了吗,这个dexElements就是从dexPath来的,也就说是
IncrementalClassLoader
用来加载dexPath(/data/data/package_name/files/instant-run/dex/)下面的dex文件。感兴趣的同学可以看下,我们app中的所有第三方库和自己项目中的代码,都被打包成若干个slice dex分片,该目录下有几十个dex文件。每当修改代码用
Instant run
完成编译,该目录下的dex文件就会有一个或者几个的更新时间发生改变。
正常情况下,apk被安装之后,APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name/base-1.apk )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类。而使用
Install run
则完全不管之前的加载路径,所有的分片dex文件和资源都在dexPath下,用
IncrementalClassLoader
去加载。也就是加载不存在APK固定路径之外的类,即动态加载。
但是仅仅有ClassLoader是不够的。因为每个被修改的类都被改了名字,类名在原名后面添加
app/build/intermediates/transforms/instantRun/debug/folders/4000
。AndroidManifest中并没有注册这些被改了名字的Activity。& 因此正常情况下系统无法加载我们插件中的类;因此也没有办法创建Activity的对象。
解决这个问题有两个思路,要么全盘接管这个类加载的过程;要么告知系统我们使用的插件存在于哪里,让系统帮忙加载;这两种方式或多或少都需要干预这个类加载的过程。
动态加载的两种方案
先来看下系统如何完成类的加载过程。
的创建过程
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);StrictMode.incrementExpectedActivityCount(activity.getClass());r.intent.setExtrasClassLoader(cl);
ClassLoader
和类名加载,反射调用生成
对象,其中的
ClassLoader
的一个对象
r.packageInfo
中获得的。
对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的
等组件的信息我们都可以通过此对象获取。
r.packageInfo的来源:
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
// 获取userid信息
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
// 尝试获取缓存信息
WeakReference&LoadedApk&
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
ref = mResourcePackages.get(aInfo.packageName);
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
// 缓存没有命中,直接new
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
// 省略。。更新缓存
return packageI
重要的是这个缓存
packageInfo
就是从这个缓存中取的,所以我们只要在
修改里面的
ClassLoader
控制类的加载就能完成动态加载。
》一文中,作者已经提出两种动态加载的解决方案:
『激进方案』中我们自定义了插件的ClassLoader,并且绕开了Framework的检测;利用ActivityThread对于LoadedApk的缓存机制,我们把携带这个自定义的ClassLoader的插件信息添加进mPackages中,进而完成了类的加载过程。
『保守方案』中我们深入探究了系统使用ClassLoader findClass的过程,发现应用程序使用的非系统类都是通过同一个PathClassLoader加载的;而这个类的最终父类BaseDexClassLoader通过DexPathList完成类的查找过程;我们hack了这个查找过程,从而完成了插件类的加载。
激进方案由于是一个插件一个
Classloader
ClassLoader
方案,代表作
;保守方案也叫做单
ClassLoader
方案,代表作,Small、众多热更新框架如
Instant run的重启更新机制
绕了一大圈,终于能接着往下看了。接上面,我们继续看
BootstrapApplication
public void onCreate() {
MonkeyPatcher.monkeyPatchApplication(
BootstrapApplication.this, BootstrapApplication.this,
realApplication, externalResourcePath);
MonkeyPatcher.monkeyPatchExistingResources(BootstrapApplication.this,
externalResourcePath, null);
super.onCreate();
//手机客户端app和Android Studio建立Socket通信,AS是客户端发消息,app
//是服务端接收消息作出相应操作。Instant run的通信方式。不在本文范围内
Server.create(AppInfo.applicationId, BootstrapApplication.this);
if (realApplication != null) {
//还记得这个realApplication吗,我们app中实际的Application
realApplication.onCreate();
上面代码,手机客户端app和Android Studio建立Socket通信,AS是客户端发消息,app是服务端接收消息作出相应操作,这是Instant run的通信方式,不在本文范围内。然后反射调用实际
Application
那么前面的两个
MonkeyPatcher
的方法是干嘛的呢
MonkeyPatcher.monkeyPatchApplication
public static void monkeyPatchApplication(@Nullable Context context,
@Nullable Application bootstrap,
@Nullable Application realApplication,
@Nullable String externalResourceFile) {
// Find the ActivityThread instance for the current thread
Class&?& activityThread = Class.forName(&android.app.ActivityThread&);
Object currentActivityThread = getActivityThread(context, activityThread);
// Find the mInitialApplication field of the ActivityThread to the real application
Field mInitialApplication = activityThread.getDeclaredField(&mInitialApplication&);
mInitialApplication.setAccessible(true);
Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
if (realApplication != null && initialApplication == bootstrap) {
//**2.替换掉ActivityThread.mInitialApplication**
mInitialApplication.set(currentActivityThread, realApplication);
// Replace all instance of the stub application in ActivityThread#mAllApplications with the
// real one
if (realApplication != null) {
Field mAllApplications = activityThread.getDeclaredField(&mAllApplications&);
mAllApplications.setAccessible(true);
List&Application& allApplications = (List&Application&) mAllApplications
.get(currentActivityThread);
for (int i = 0; i & allApplications.size(); i++) {
if (allApplications.get(i) == bootstrap) {
//**1.替换掉ActivityThread.mAllApplications**
allApplications.set(i, realApplication);
// Figure out how loaded APKs are stored.
// API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
Class&?& loadedApkC
loadedApkClass = Class.forName(&android.app.LoadedApk&);
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName(&android.app.ActivityThread$PackageInfo&);
Field mApplication = loadedApkClass.getDeclaredField(&mApplication&);
mApplication.setAccessible(true);
Field mResDir = loadedApkClass.getDeclaredField(&mResDir&);
mResDir.setAccessible(true);
// 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices
// floating around.
Field mLoadedApk = null;
mLoadedApk = Application.class.getDeclaredField(&mLoadedApk&);
} catch (NoSuchFieldException e) {
// According to testing, it's okay to ignore this.
// Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
// ActivityThread#mResourcePackages and do two things:
- Replace the Application instance in its mApplication field with the real one
- Replace mResDir to point to the external resource file instead of the .apk. This is
used as the asset path for new Resources objects.
- Set Application#mLoadedApk to the found LoadedApk instance
for (String fieldName : new String[]{&mPackages&, &mResourcePackages&}) {
Field field = activityThread.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(currentActivityThread);
for (Map.Entry&String, WeakReference&?&& entry :
((Map&String, WeakReference&?&&) value).entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
if (mApplication.get(loadedApk) == bootstrap) {
if (realApplication != null) {
//**3.替换掉mApplication**
mApplication.set(loadedApk, realApplication);
if (externalResourceFile != null) {
//替换掉资源目录
mResDir.set(loadedApk, externalResourceFile);
if (realApplication != null && mLoadedApk != null) {
//**4.替换掉mLoadedApk**
mLoadedApk.set(realApplication, loadedApk);
} catch (Throwable e) {
throw new IllegalStateException(e);
这里做了三件事情:
1.替换Application对象
BootstrapApplication
的作用就是加载
realApplication
MyApplication
,所以我们就要把所有Framework层的
BootstrapApplication
对象替换为
MyApplication
对象。包括:
baseContext.mPackageInfo.mApplication 代码3处baseContext.mPackageInfo.mActivityThread.mInitialApplication 代码2处baseContext.mPackageInfo.mActivityThread.mAllApplications 代码1处
2.替换资源相关对象mResDir,前面我们已经说过,正常情况下寻找资源都是在
/data/app/package_name/base-1.apk
目录下,而
Instant run
将资源也抽出来放在
/data/data/package_name/files/instant-run/
,加载目录也更改为后者
mLoadedApk
还记得前面的讲的
吗,这里面有加载类的
ClassLoader
BootstrapApplication
attachBaseContext
方法中就将其已经替换为了
IncrementalClassLoader
,所以代码4处反射将
BootstrapApplication
mLoadedApk
MyApplication
,那么接下来MyApplication的所有类的加载都将由
IncrementalClassLoader
MonkeyPatcher.monkeyPatchExistingResources
更新资源补丁,不在本文范围内就不讲了。
这些工作做完之后调用
MyApplication
BootstrapApplication
就将控制权交给了
MyApplication
,这样在整个运行环境中,
MyApplication
Application
Application
总结一下,刚才我们说了已经有两个动态加载的方案,激进方案和保守方案,而
Instant run
的重启更新机制更像后者–保守方案即单
ClassLoader
方案,首先,该种方案只有一个
ClassLoader
,只不过是通过替换
Application
达到的替换
mLoadedApk
ClassLoader
的目的,并没有涉及到缓存
然后dexList也是它自己维护的。
Instant run 热更新机制
Instant run哪里用到的热更新机制呢?还记得刚才我们提到的Socket通信吗,其中S端也就是手机客户端,接收到热更新的消息会执行下面的方法:
private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
//新建一个ClassLoader,dexFile是刚更新的插件
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
mApplication.getCacheDir().getPath(), nativeLibraryPath,
getClass().getClassLoader());
// we should transform this process with an interface/impl
Class&?& aClass = Class.forName(
&com.android.tools.fd.runtime.AppPatchesLoaderImpl&, true, dexClassLoader);
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod(&getPatchedClasses&).invoke(loader);
//loader是PatchesLoader的一个实例,调用load方法加载插件
if (!loader.load()) {
updateMode = UPDATE_MODE_COLD_SWAP;
} catch (Exception e) {
updateMode = UPDATE_MODE_COLD_SWAP;
} catch (Throwable e) {
updateMode = UPDATE_MODE_COLD_SWAP;
return updateM
可以看到根据单个dexFile新建了一个
ClassLoader
,然后调用
loader.load()
PatchesLoader
接口的实例,
PatchesLoader
接口的一个实现类
AppPatchesLoaderImpl
,该类中记录了哪些修改的类。看一下
public boolean load() {
//遍历已记录的所有修改的类
for (String className : getPatchedClasses()) {
ClassLoader cl = getClass().getClassLoader();
//我们刚才说的修改的类名后面都有$override
Class&?& aClass = cl.loadClass(className + &$override&);
Object o = aClass.newInstance();
//1.**反射修改原类中的$change字段为修改后的值**
Class&?& originalClass = cl.loadClass(className);
Field changeField = originalClass.getDeclaredField(&$change&);
// force the field accessibility as the class might not be &visible&
// from this package.
changeField.setAccessible(true);
// If there was a previous change set, mark it as obsolete:
Object previous = changeField.get(null);
if (previous != null) {
Field isObsolete = previous.getClass().getDeclaredField(&$obsolete&);
if (isObsolete != null) {
isObsolete.set(null, true);
changeField.set(null, o);
} catch (Exception e) {
return false;
return true;
Instant run
的热更新原理可以概述为:
1.第一次运行,应用
transform API
修改字节码。
输出目录在
app/build/intermediates/transforms/instantRun/debug/folders/1/
,给所有的类添加
IncrementalChange
IncrementalChange
是个接口。如果
不为空,去调用
access$dispatch
方法,参数为方法签名字符串和方法参数数组,否则调用原逻辑。
load方法中会去加载全部补丁类,并赋值给对应原类的
这也验证了我们说它是多
ClassLoader
2.所有修改的类有
gradle plugin
自动生成,类名在原名后面添加$override,复制修改后类的大部分方法,实现IncrementalChange 接口的access$dispatch方法,该方法会根据传递过来的方法签名,调用本类的同名方法。
那么也就是说只要把原类的
字段设置为该类,那就会调用该类的
access$dispatch
方法,就会使用修改后的方法了。上面代码1处就通过反射修改了原类中的
为修改后补丁类中的值。
AppPatchesLoaderImpl
记录了所有被修改的类,也会被打进补丁dex。
总结一下,可以看到
Instant run
热更新是多
ClassLoader
加载方案,每个插件dex都有一个
ClassLoader
,如果插件需要升级,直接重新创建一个自定的
ClassLoader
加载新的插件。但是目前来看,
Instant run
修改java代码大部分情况下都是重启更新机制,可能热更新机制还有bug。资源更新是热更新,重启对应Activity就可以。
Instant run
看下来真的有好多东西,其中就以替换
Application
和动态加载尤为重要,关于动态加载,完全可以根据
Instant run
的实现方式完成一个热修复和重启修复相结合的更新框架,用于线上bug的修复和功能更新,并且可以支持资源文件的更新,是无侵入性的更新框架,最重要的一点,这是官方支持的。但是,性能肯定会有所影响,实际开发中使用
Instant run
编译其实还有很多的问题,而且app初始化时使用的很多反射,这也直接导致app的启动速度降低好多。
另外一点关于Application的替换是基于
(一种构建工具,类似于burk)中的
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致逆向学习(4)
将apk的后缀修改为zip,解压缩后会生成如下两个文件夹和三个文件
& &&& META-INF & & & & & & & & & & & & & & & & & &
& & &&res & & & & & & & & & & & & & & & res文件夹下包含图片,音频,字库等资源,及处理后的布局文件,暂时按下稍后会有文章介绍解决方法
& & & AndroidManifest.xml&& & &打开AndroidManifest.xml会发现是乱码,暂时按下稍后会有文章介绍解决方法
& & &&classes.dex & & & & & & & & & 这个文件就是我们的目标,classes.dex是java源码编译后生成的java字节码文件.但由于Android使用的dalvik虚拟机与标准的java虚拟机是不兼容的,dex文件与class文件相比,不论是文件结构还是opcode都不一样.
& & &&resources.arsc & & & & & & & 文本资源管理,有专门的工具(ArscEditor)可以进行处理
Step 2 从classes.dex转制成jar文件
首先下载工具dex2jar 下载地址为&&这个工具可以将classes.dex文件转换成jar文件。
接压缩之后在命令行里,指定到解压缩的目录,运行如下脚本
就会在你的dex2jar目录下生成classes-dex2jar.jar文件
Step 3 使用jd-gui获取java代码
jd-gui是一个java反编译软件可以从&&下载
打开jd-gui 读取刚刚生成的classes-dex2jar.jar,点击Save All Sources生成源代码的zip包。
这种方式反编译安卓代码,有这么几个问题
大部分代码都会经过混淆处理,代码阅读性差jd-gui在解释switch case时候会出现逻辑错误
&特别说明:
用UiAutomator编译出来的jar包,也用这个方法来反编译。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:32265次
排名:千里之外
原创:128篇
转载:17篇
(1)(4)(1)(4)(8)(5)(6)(1)(3)(1)(1)(4)(3)(104)(1)(2)

我要回帖

更多关于 classes.dex修改 的文章

 

随机推荐