• dexposed框架Android在线热修复


    移动client应用相对于Webapp的最大一个问题每次出现bug,不能像web一样在server就完毕修复,不须要发版本号。紧急或者有安全漏洞的问题,

    假设是Webapp你可能最多花个1,2个小时紧急公布上线。可是app呢,打包,跪求市场公布几百个渠道,周末还发不了。app配置升级。你还不能配置

    强制升级。 就算配置提示升级,用户心里肯定想前两天刚升级最新版,怎么又要升。并且升级要流量。这时候会非常反感甚至卸载应用。所以安卓是否

    有能力做到在线打补丁?dexposed给我们攻克了这个问题。


    1、淘宝Dexposed框架

    喜欢刷机的人应该对xposed不陌生,Xposed框架是一款能够在不改动APK的情况下影响程序执行(改动系统)的框架服务。基于它 能够制作出很多功

    能强大的模块。

    事实上Xposed安卓的一个开源框架,在github上的下载地址xposed,有兴趣的能够去研究下,dexPosed也是基于Xposed的。

    他有几个典型的使用场景:

    a. Classic AOP programming (aop编程)

    b. Instrumentation (for testing, performance monitoring and etc.)  測试,性能监控

    c. Online hot patch to fix critical, emergent or security bugs 线上打补丁,解决一些严重的,紧急的或者安全漏洞的bug。

    d. SDK hooking for a better development experience


    对于aop编程我这里就不多说了,deXposed提供的 是无侵入性的,并且AOP就是用的java代码。相比国外比較流行的Aspectj有几点优势:

    a、无侵入性。

    b、使用Java代码编写,Aspectj使用脚本须要一定的学习成本

    c、Aspectj有自己的编译器,须要编译下Aspectj代码。会注入他自己的代码


    以下来看看假设写一些AOP的代码:

    Attach a piece of code before and after all occurrences of Activity.onCreate(Bundle). 



    beforeHookedMethod和afterHookedMethod方法做一些详细的操作。我们能够看到,用dexposed能够实现不改变原函
    数的运行。可是在原函数运行前后去做一些其它的额外处理。比如改变入參和返回值等等的一些事情。


      Replace the original body of the target method.
    
        DexposedBridge.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodReplacement() {
    
            @Override protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
                // Re-writing the method logic outside the original method context is a bit tricky but still viable.
                ...
            }
    
        });


    则是能够将原有要运行的函数替换成一个我们须要的新的运行函数。


    这个框架眼下对android 5.0系统支持度不高。只是框架更新的时间能够看出,持续维护中,不久将对art全然支持。

    他提供一个函数来检測你的android

    系统是否支持 Dexposed,DexposedBridge.canDexposed(context)。

    你须要把补丁包打包成一个apk。 然后通过以下的代码来载入补丁包:

    // Run taobao.patch apk
        public void runPatchApk() {
            if (android.os.Build.VERSION.SDK_INT == 21) {
                return;
            }
            if (!isSupport) {
                Log.d("dexposed", "This device doesn't support dexposed!");
                return;
            }
            File cacheDir = getExternalCacheDir();
            if (cacheDir != null) {
                String fullpath = cacheDir.getAbsolutePath() + File.separator + "patch.apk";
                PatchResult result = PatchMain.load(this, fullpath, null);
                if (result.isSuccess()) {
                    Log.e("Hotpatch", "patch success!");
                } else {
                    Log.e("Hotpatch", "patch error is " + result.getErrorInfo());
                }
            }
        }


    2.Dexposed原理

    重点是搞清楚是如何hook的。

    1)首先通过ClassLoader把插件apk载入进入主project

    2)通过反射拿到详细的class类

    3)DexposedBridge.findAndHookMethod定位到详细的方法中,对代码进行覆盖或者增加自己定义功能

    前面两点都是Java方法没什么能够说的,我这里重点分析下第三点:

    a、DexposedBridge.findAndHookMethod()中调用了Xc_methdhook.hookMethos(),这种方法是通过传入的class。方法及签名去查找返回一个相应的Method。

    然后把Method作为參数传入hookMethodNative(Method  ...);

    b、hookMethodNative是进行hook的主要方法。分析源代码发现,它里面做了这些事情

    1、把Java层的变量,类型转换成c能识别的指针类型。

        // Save a copy of the original method and other hook info
        DexposedHookInfo* hookInfo = (DexposedHookInfo*) calloc(1, sizeof(DexposedHookInfo));
        memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));
        hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));
        hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));

    2、用我们自己的code取代,看到以下的nativeFun了吗。这种方法被dexposedCallHandler取代。也就是说被我们自己的回调方法取代了。这样我们就能够

    任意的操作了。然后把要hook的对象參数保存到insns

       // Replace method with our own code
        SET_METHOD_FLAG(method, ACC_NATIVE);
        method->nativeFunc = &dexposedCallHandler;
        method->insns = (const u2*) hookInfo;
        method->registersSize = method->insSize;
        method->outsSize = 0;

    3、接下来我们去看看dexposedCallHandler做了哪些事情;

     //赋值
    DexposedHookInfo* hookInfo = (DexposedHookInfo*) method->insns;
        Method* original = (Method*) hookInfo;
        Object* originalReflected = hookInfo->reflectedMethod;
        Object* additionalInfo = hookInfo->additionalInfo;

     // call the Java handler function
    //对...这才是重点。dvmCallMethod方法回调了dexposedHandleHookedMethod方法,这种方法是在Java层实现的
     JValue result;
        dvmCallMethod(self, dexposedHandleHookedMethod, NULL, &result,
            originalReflected, (int) original, additionalInfo, thisObject, argsArray);
            
        dvmReleaseTrackedAlloc((Object *)argsArray, self);
    


    4、我们在去看看handleHookMethod做了什么。一看你就明朗了;

    private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
    			Object thisObject, Object[] args) throws Throwable {
    		AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;
    
    		if (disableHooks) {
    			try {
    				return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,
    						additionalInfo.returnType, thisObject, args);
    			} catch (InvocationTargetException e) {
    				throw e.getCause();
    			}
    		}
    
    		Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot();
    		final int callbacksLength = callbacksSnapshot.length;
    		if (callbacksLength == 0) {
    			try {
    				return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,
    						additionalInfo.returnType, thisObject, args);
    			} catch (InvocationTargetException e) {
    				throw e.getCause();
    			}
    		}
    
    		MethodHookParam param = new MethodHookParam();
    		param.method  = method;
    		param.thisObject = thisObject;
    		param.args = args;
    
    		// call "before method" callbacks
    		int beforeIdx = 0;
    		do {
    			try {
    				((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
    			} catch (Throwable t) {
    				DexposedBridge.log(t);
    
    				// reset result (ignoring what the unexpectedly exiting callback did)
    				param.setResult(null);
    				param.returnEarly = false;
    				continue;
    			}
    
    			if (param.returnEarly) {
    				// skip remaining "before" callbacks and corresponding "after" callbacks
    				beforeIdx++;
    				break;
    			}
    		} while (++beforeIdx < callbacksLength);
    
    		// call original method if not requested otherwise
    		if (!param.returnEarly) {
    			try {
    				param.setResult(invokeOriginalMethodNative(method, originalMethodId,
    						additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));
    			} catch (InvocationTargetException e) {
    				param.setThrowable(e.getCause());
    			}
    		}
    
    		// call "after method" callbacks
    		int afterIdx = beforeIdx - 1;
    		do {
    			Object lastResult =  param.getResult();
    			Throwable lastThrowable = param.getThrowable();
    
    			try {
    				((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
    			} catch (Throwable t) {
    				DexposedBridge.log(t);
    
    				// reset to last result (ignoring what the unexpectedly exiting callback did)
    				if (lastThrowable == null)
    					param.setResult(lastResult);
    				else
    					param.setThrowable(lastThrowable);
    			}
    		} while (--afterIdx >= 0);
    
    		// return
    		if (param.hasThrowable())
    			throw param.getThrowable();
    		else
    			return param.getResult();
    	}

    推断是否hook成功。假设不成功运行invokeOriginalMethodNative,也就是运行原始函数;

    成功则,这个函数查找被挂钩函数的挂钩 XC_MethodHook结构体,然后运行里边的 beforeHookedMethod函数。再通过 invokeOriginalMethodNative

    运行挂钩前的原始函数。最后再运行 afterHookedMethod 函数。

    findAndHookMethod 的实现就分析完了,

    本质上仍然是寻找被挂钩函数的 Method 结构体,将Method属性改为native ,然后对其成员 nativeFunc,

    registersize 等进行赋值,当中 insns 成员保存了挂钩的具体信息。全部被挂钩的函数。其nativeFunc都赋值为 dexposedCallHandler 函数,该函数终于运行 XposedBridge.class 里的 handleHookedMethod 。 handleHookedMethod 寻找dexposed模块及dexposed框架调用 findAndHookMethod 注冊的 before,after

    函数,假设有,就运行,再通过invokeOriginalMethodNative 运行挂钩前函数。


    3、怎么使用dexposed

    a、下载dexposed源代码 dexposed  

    b、把下载的源代码导入,仅仅须要sample包。当中dexposedsample是有bug的主project,patchsample为插件project,(其它的两个为工具类源代码,方便我们理解源代码)

    c、主project配置gradle文件

    apply plugin: 'com.android.application'
    android {
        compileSdkVersion 21
        buildToolsVersion "21.1.2"
    
        defaultConfig {
            applicationId "com.taobao.dexposed"
            minSdkVersion 9
            targetSdkVersion 21
            versionName "1.0"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    
        android {
            packagingOptions {
                exclude 'AndroidManifest.xml'
            }
    
        }
    
        buildTypes {
            //Debug打开,使用測试环境接口
            debug {
            }
        }
        lintOptions {
            abortOnError false
        }
    
        sourceSets {
            main {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java.srcDirs = ['src/main/java']
                res.srcDirs = ['src/main/res']   //路径依据自己的相应改动
            }
        }
    }
    
    
    //我使用的是本地导入so包。
    task copyNativeLibs(type: Copy) {
        from fileTree(dir: 'libs', include: '*/*.so' )  into  'build/native-libs'
    }
    tasks.withType(JavaCompile) { compileTask -> compileTask.dependsOn copyNativeLibs }
    
    clean.dependsOn 'cleanCopyNativeLibs'
    
    tasks.withType(com.android.build.gradle.tasks.PackageApplication) {
            pkgTask ->
            pkgTask.jniFolders = new HashSet<File>()
            pkgTask.jniFolders.add(new File(projectDir, 'libs'))
    }
    //---------------------------------
    //这段配置是把主project打成jar包。这个jar包须要导入到patchproject中
     task clearJar(type: Delete) {
        delete 'libs/sdk.jar'
    }
    
    task makeJar(type:org.gradle.api.tasks.bundling.Jar) {
        //指定生成的jar名
        baseName 'sdk'
        //从哪里打包class文件
        from('build/intermediates/classes/sample/debug/com')
        //打包到jar后的文件夹结构,依据自己project须要写路径
        into('com')
        //去掉不须要打包的文件夹和文件
        exclude('test/', 'BuildConfig.class', 'R.class')
        //去掉R$开头的文件
        exclude{ it.name.startsWith('R$');}
    }
    
    makeJar.dependsOn(clearJar, build)
    //---------------------------------------------
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile files('libs/dexposedbridge.jar')
    }
    
    patchprojectgradle配置:

    dependencies {
    //        compile fileTree(dir: 'libs', include: ['*.jar'])
            provided files('libs/sdk.jar')
            provided files('libs/dexposedbridge.jar')
            provided files('libs/patchloader.jar')
        }
    

    使用provided方式导入jar包,这样仅仅会在编译时引用jar,不会把jar打包进patch.apk。防止与主project引入包冲突;


    用例代码就不贴了,自己去down一份


    4、关于patch包的安全问题能够看Android Hotpatch



  • 相关阅读:
    【小程序开发】基本信息页面源码(头像、二维码上传,省市县地区选择,公司选择,名字输入等)
    【小程序开发】购物车加减几件demo
    【前端开发】面向对象编程案例创建对象
    【前端活动】活动常见形式及案例
    【前端开发】限制input输入保留两位小数
    react-native保存图片Android实现方法
    echarts柱状图宽度设置(react-native)
    echarts饼图字体大小修改
    ATOM常用插件推荐
    echarts柱状图的整体高度怎么设置
  • 原文地址:https://www.cnblogs.com/slgkaifa/p/7026057.html
Copyright © 2020-2023  润新知