我们知道,在Android操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步的向下执行。而“钩子”的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子勾上事件一样。并且能够在勾上事件时,处理一些自己特定的事件。如下图所示:
传统的静态代理模式需要为每一个需要代理的类写一个代理类,如果需要代理的类有几百个那不是要累死?为了更优雅地实现代理模式,JDK提供了动态代理方式,可以简单理解为JVM可以在运行时帮我们动态生成一系列的代理类,这样我们就不需要手写每一个静态的代理类了。依然以购物为例,用动态代理实现如下:
public static void main(String[] args) {
Shopping people = new ShoppingImp();
System.out.println(Arrays.toString(people.doShopping(100)));
people = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(),
people.getClass().getInterfaces(), new ShoppingHandler(people));
System.out.println(Arrays.toString(people.doShopping(100)));
}
Android在启动的时候会创建ActivityThread
, 这是一个单例的对象,而startActivity
实际上是Instrumentation
中的execStartActivity()
来实现的。所有我们只要替换掉ActivityThread
中的Instrumentation
的对象成我们自己的方法。
创建代理类
public class ProxyInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;
public ProxyInstrumentation(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options){
// Hook之前, XXX到此一游!
Log.d(TAG, "sanfen到此一游!!!");
Log.d(TAG, "\n执行了startActivity, 参数如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
// 某该死的rom修改了 需要手动适配
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
通过反射修改ActivityThread
中的mInstrumentation
。
public static void hookStartActivity(){
try {
// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 创建代理对象
Instrumentation evilInstrumentation = new ProxyInstrumentation(mInstrumentation);
// 偷梁换柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InvocationTargetException
| NoSuchFieldException e) {
e.printStackTrace();
}
}
执行效果,在运行startActivty的时候打出了一段日志。
AndFix采用native hook的方式,这套方案直接使用dalvik_replaceMethod替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换init与clinit只可以修改field的数值)。
也正因如此,Andfix可以支持的补丁场景相对有限,仅仅可以使用它来修复特定问题。结合之前的发布流程,我们更希望补丁对开发者是不感知的,即他不需要清楚这个修改是对补丁版本还是正式发布版本(事实上我们也是使用git分支管理+cherry-pick方式)。另一方面,使用native替换将会面临比较复杂的兼容性问题。
在gradle中添加依赖
dependencies {
compile 'com.alipay.euler:andfix:0.5.0@aar'
}
public class AndFixApplication extends Application {
public static PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
// 初始化patch管理类
mPatchManager = new PatchManager(this);
// 初始化patch版本
mPatchManager.init("1.0");
// String appVersion = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
// mPatchManager.init(appVersion);
// 加载已经添加到PatchManager中的patch
mPatchManager.loadPatch();
}
}
为了方便演示,我们设置点击按钮来加载patch
public class MainActivity extends AppCompatActivity {
private static final String APATCH_PATH = "/fix.apatch"; // 补丁文件名
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.load).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
update();
}
});
}
private void update() {
String patchFileStr = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
try {
AndFixApplication.mPatchManager.addPatch(patchFileStr);
} catch (IOException e) {
e.printStackTrace();
}
}
}
patch命令
- -f <new.apk> :新apk
- -t <old.apk> : 旧apk
- -o : 输出目录(补丁文件的存放目录)
- -k : 打包所用的keystore
- -p : keystore的密码
- -a : keystore 用户别名
- -e : keystore 用户别名密码
sh apkpatch.sh -f app-debug-2.0.apk -t app-debug-1.0.apk -o output -k abc.keystore -p qwe123 -a abc.keystore -e qwe123
load patch
#安装应用
adb install app-debug-1.0.apk
#将patch push到手机中
adb push fix.apatch /storage/emulated/0/fix.apatch
插桩的概念是以静态的方式修改第三方的代码,也就是从编译阶段,对源代码(中间代码)进行编译,而后重新打包,是静态的篡改; 而hook则不需要再编译阶段修改第三方的源码或中间代码,是在运行时通过反射的方式修改调用,是一种动态的篡改
插桩就是在代码中插入一段我们自定义的代码。
#反编译出smail文件
java -jar apktool.jar d <包名>
#重新打包
java -jar apktool.jar b <文件夹> -o <输出包名>
#使用keytool生成签名
keytool -genkey -alias abc.keystore -keyalg RSA -validity 20000 -keystore abc.keystore
#用jarsigner签名apk
jarsigner -verbose -keystore abc.keystore -signedjar test_sign.apk test.apk abc.keystore
对于Java程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的class文件),其中起到关键作用的就是类加载器ClassLoader。
Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader classLoader = getClassLoader();
if (classLoader != null){
Log.i(TAG, "[onCreate] classLoader " + " : " + classLoader.toString());
while (classLoader.getParent()!=null){
classLoader = classLoader.getParent();
Log.i(TAG,"[onCreate] classLoader " + " : " + classLoader.toString());
}
}
}
05-08 16:22:43.115 10289-10289/com.example.fensan.classloader I/MainActivity: [onCreate] classLoader : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.fensan.classloader-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.example.fensan.classloader-1, /vendor/lib, /system/lib, /data/datalib]]]
05-08 16:22:43.115 10289-10289/com.example.fensan.classloader I/MainActivity: [onCreate] classLoader : java.lang.BootClassLoader@415cb330
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
从源码中我们也可以看出,loadClass方法在加载一个类的实例的时候,
- 会先查询当前ClassLoader实例是否加载过此类,有就返回;
- 如果没有。查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
- 如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;
用来加载安装了的应用中的dex文件。它也是Android里面的一个最核心的ClassLoader了。相当于Java中的那个AppClassLoader。
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
它的实例化是通过调用ApplicationLoaders.getClassLoader
来实现的。
它是在ActivityThread
启动时发送一个BIND_APPLICATION
消息后在handleBindApplication
中创建ContextImpl
时调用LoadedApk
里面的getResources(ActivityThread mainThread)
最后回到ActivityThread
中又调用LoadedApk
的getClassLoader
生成的,具体的在LoadedApk的createOrUpdateClassLoaderLocked
。
那么问题来了,当Android加载class的时候,LoadedApk
中的ClassLoader
是怎么被调用到的呢?
其实Class里面,如果你不给ClassLoader
的话,它默认会去拿Java虚拟机栈里面的CallingClassLoader
,而这个就是LoadedApk里面的同一个ClassLoader
。
它是一个可以用来加载包含dex文件的jar或者apk文件的,但是它可以用来加载非安装的apk。比如加载sdcard上面的,或者NetWork的。
比如现在很流行的插件化/热补丁,其实都是通过DexClassLoader来实现的。具体思路是: 创建一个DexClassLoader,通过反射将前者的DexPathList跟系统的PathClassLoader中的DexPathList合并,就可以实现优先加载我们自己的新类,从而替换旧类中的逻辑了。
findViewById(R.id.loadjar)
.setOnClickListener(v -> {
try {
File sourceFile = new File(
Environment.getExternalStorageDirectory() + File.separator
+ "dex.jar");// 导出的jar的存储位置
File file = getDir("osdk", 0);// dex临时存储路径
DexClassLoader dexClassLoader = new DexClassLoader(sourceFile.getAbsolutePath(), file.getAbsolutePath(), null,
getClassLoader());
Class<?> libProviderClazz = dexClassLoader
.loadClass("com.interfaces.InterfaceTest");
MainInterface mMainInterface = (MainInterface) libProviderClazz
.newInstance();// 接口
String str = mMainInterface.sayHello();// 获取jar包提供的数据
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。 理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类
简单来说,在编译时通过新旧两个Dex生成差异path.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch中。为了补丁包尽量的小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。它的粒度是Dex格式的每一项,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/QZone的粒度为class。
#添加依赖
dependencies {
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
}
#添加依赖
dependencies {
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
}
#设置版本
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-1018-17-32-47.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-1018-17-32-47-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
使用ApplicationLike代理Appilcation
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
private static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}
加载本地的补丁包
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
- 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。