精华内容
下载资源
问答
  • AndFix

    2017-02-12 23:48:35
    AndFix 是阿里巴巴开源的 Android 应用热修复工具,帮助 Anroid 开发者修复应用的线上问题。Andfix 是 “Android hot-fix” 的缩写。支持 Android 2.3 - 6.0,ARM 和 x86 架构,dalvik 运行时和 art 运行时。AndFix ...

    AndFix 是阿里巴巴开源的 Android 应用热修复工具,帮助 Anroid 开发者修复应用的线上问题。Andfix 是 “Android hot-fix” 的缩写。支持 Android 2.3 - 6.0,ARM 和 x86 架构,dalvik 运行时和 art 运行时。AndFix 的分支是 .apatch 文件。



    GitHub地址 : https://github.com/alibaba/AndFix

    原理图:

    使用概述

    1. 添加依赖

    dependencies {    compile 'com.alipay.euler:andfix:0.3.1@aar'}

    2 . 尽可能早的加载补丁

    package com.dyk.andfixtest;import android.app.Application;import android.content.pm.PackageManager;import com.alipay.euler.andfix.patch.PatchManager;/**
     * Created by dyk on 2016/3/24.
     */public class MyApplication extends Application {
    
        private static MyApplication instance;    private PatchManager patchManager;    public static MyApplication getInstance(){        return instance;
        }    @Override
        public void onCreate() {        super.onCreate();
            instance = this;        // 初始化patch管理类
            patchManager = new PatchManager(this);
            String appVersion = null;        try {
                appVersion = getPackageManager().getPackageInfo(getPackageName(),0).versionName;            // 初始化patch版本
                patchManager.init(appVersion);            // 加载已经添加到PatchManager中的patch
                patchManager.loadPatch();
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
    
        }    public PatchManager getPatchManager() {        return patchManager;
        }
    }

    每次appVersion变更都会导致所有补丁被删除,如果appversion没有改变,则会加载已经保存的所有补丁。

    3. 加载新补丁

    MyApplication.getInstance().getPatchManager().addPatch(patchPath);

    详细使用教程

    1 . 添加依赖

    2. 尽可能早的加载补丁

    3. 修复bug,生成没有bug的apk文件

    4. 对比新旧apk生成.apatch补丁文件

    5. 加载新补丁,修复bug

    前两步在使用概述中已经说明,不在赘述。修复bug,生成新apk也和正常一样。这里注意一个地方:生成新旧apk要使用同一个.jks签名文件。下面是一个示例

    从一个Demo开始

    制造bug

    下面的代码假设在waitFix()里存在bug(Log.i(TAG, “waitFix bug”),我们目标是不发布新版本,改变这行log)

    package com.dyk.andfixtest;import android.app.Activity;import android.os.Bundle;import android.os.Environment;import android.util.Log;import android.view.View;import android.view.Window;import java.io.IOException;public class MainActivity extends Activity {
    
        private static final String TAG = "AndFix";    @Override
        protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);
            requestWindowFeature(Window.FEATURE_NO_TITLE);
            setContentView(R.layout.activity_main);
            findViewById(R.id.showLogBtn).setOnClickListener(new View.OnClickListener() {            @Override
                public void onClick(View v) {
                    waitFix();
                }
            });
    
            findViewById(R.id.fixBtn).setOnClickListener(new View.OnClickListener() {            @Override
                public void onClick(View v) {
                    String patchPath = Environment.getExternalStorageDirectory()+"/tmp/first.apatch";                try {
                        MyApplication.getInstance().getPatchManager().addPatch(patchPath);
                        Log.i(TAG,"fix bug, please reClick");
                    } catch (IOException e) {
                        e.printStackTrace();
                        Log.i(TAG,"error:"+e.toString());
                    }
                }
            });
    
        }    // 假设待修复的Bug在此方法中
        private void waitFix() {
            Log.i(TAG,"waitFix bug");
        }
    }

    因为要读取.apatch文件,不要忘了添加限权。

        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    此时我们签名运行apk,生成bug.apk。

    // 已修复private void waitFix() {
         Log.i(TAG,"fix bug");
    }

    修改bug后生成fix.apk。

    生成.aptch补丁文件需要一个工具,点我下载。现在打开命令行(cmd),进入从刚下载工具的tools文件夹(我的是D:\Temp\AndFix\AndFix-master\tools,命令行为:d:回车,cd D:\Temp\AndFix\AndFix-master\tools)。将bug.apk、fix.apk和签名文件放入tools文件夹下。输入命令

    apkpatch -o D:\Temp\AndFix\output -k AndFix.jks -p admin888 -a 亚洲 -e admin888 -f fix.apk -t bug.apk
    
    字段说明:
    -o <output> : 输出目录-k <keystore>: 打包所用的keystore-p <password>: keystore的密码-a <alias>: keystore 用户别名-e <alias password>: keystore 用户别名密码-f <new.apk> :新版本-t <old.apk> : 旧版本

    看见如上提示即生成.apatch文件成功。进入输出目录(-o 后面)。可以看到一个smail文件夹、diff.dex和一个.apatch文件。经常反编译别人apk的同学一定对smail文件夹不陌生。可以查看到代码,可惜才疏学浅看不懂.smail文件。这里我们要用到的是.apatch文件。

    1. 更改.apatch文件名为first.apatch。

    2. 安装bug.apk,真是个不吉利的名字。

    3. 将first.apatch放入MainActivity里的patchPath路径下(我这里是/tmp/)。

    4. 运行bug.apk

    依次点击showLogBtn、fixBtn、showLogBtn。Log输出如下

    可以看到Log输出已经改变,这也意味着带有Bug的方法被成功修复。

    多次打补丁

    如果本地保存了多个补丁,那么AndFix会按照补丁生成的时间顺序加载补丁。具体是根据.apatch文件中的PATCH.MF的字段Created-Time。

    混淆

    -keep class * extends java.lang.annotation.Annotation-keepclasseswithmembernames class * {
        native <methods>;
    }

    探索

    1. 修改first.apatch后缀名为.zip

    2. 解压文件

    打开META-INF中的文件PATCH.MF,可以看到如下内容

    Manifest-Version: 1.0Patch-Name: fixCreated-Time: 25 Mar 2016 08:28:00 GMTFrom-File: fix.apkTo-File: bug.apkPatch-Classes: com.dyk.andfixtest.MainActivity_CFCreated-By: 1.0 (ApkPatch)

    可以发现Patch-Classes就是被改动过的文件,后面还加个_CF(CrossFire ?)。

    使用dex2jar反编译classes.dex

    1. 下载dex2jar和jd-gui,点我下载

    2. 将classes.dex文件复制到dex2jar解压目录

    3. 命令行进入dex2jar解压目录

    4. 输入命令:d2j-dex2jar classes.dex

    经过四招还我漂漂拳后,可以在dex2jar解压目录下发现classes-dex2jar.jar这么一个文件。

    使用jd-gui打开classes-dex2jar.jar

    package com.dyk.andfixtest;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.View;import com.alipay.euler.andfix.annotation.MethodReplace;public class MainActivity_CF extends Activity{
      private static final String TAG = "AndFix";  @MethodReplace(clazz="com.dyk.andfixtest.MainActivity", method="waitFix")  private void waitFix()
      {
        Log.i("AndFix", "fix bug");
      }  protected void onCreate(Bundle paramBundle)
      {    super.onCreate(paramBundle);
        requestWindowFeature(1);
        setContentView(2130968601);
        findViewById(2131492944).setOnClickListener(new MainActivity.1(this));
        findViewById(2131492943).setOnClickListener(new MainActivity.2(this));
      }
    }

    可以发现.patch只是差异文件,这里给需要替换的方法添加了一个指定class和method的注解@MethodReplace(clazz="com.dyk.andfixtest.MainActivity", method="waitFix")。客户端sdk得到补丁文件后根据注解寻找需要替换的方法。最后由JNI层完成方法的替换。

    展开全文
  • andfix

    2016-09-19 17:06:25
    Import samplesI/AndFixDemo to your IDE, append AndFixDemo dependencies with AndFix(library project or aar).Build project, save the package as 1.apk, and then install on device/emulator.Modify com.eule
    1. Import samplesI/AndFixDemo to your IDE, append AndFixDemo dependencies with AndFix(library project or aar).
    2. Build project, save the package as 1.apk, and then install on device/emulator.
    3. Modify com.euler.test.A, references com.euler.test.Fix.
    4. Build project, save the package as 2.apk.
    5. Use apkpatch tool to make a patch.
    6. Rename the patch file to out.apatch, and then copy it to sdcard.
    7. Run 1.apk and view log.


    http://www.tuicool.com/articles/yiui22u
    ART:android runtime,谷歌android4.4系统新增的一种应用运行模式,与传统的dalvilM模式不同,ART模式可以实现更为流畅的安卓系统体验
    dalvik模式:是Google公司自己设计用于android平台的虚拟机。
    dalvik虚拟机是Google等厂商合作开发的android移动设备平台的核心组成部分之一。
    它可以支持已转换为.dex(即dalvik executable)格式的java应用程序的运行
    .dex格式是专为dalvik设计的一种压缩格式,适合内存和处理器

    2014.6.25  ----------谷歌直接删除dalvik,代替他的是传闻已久的ART
    ===================================================================
    ==================================================================
    =================================================================
    AOP编程(aspect  oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
    ------------是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果
    -------是处理某个步骤和阶段的
    aaaa方面(aspect):一个关注点的模块化,这个关注点实现可能另外横切几个对象。
    事物管理是J2EE应用中很好的横切关注点例子。方面用spring的advisor或拦截器实现
    bbbb连接点(joinpoint):程序执行过程中明确的点,若方法的调用或特定的异常被抛出
    cccc通知(advice):在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”,“before“和”throws”通知。通知类型将在下面讨论。许多AOP框架包括spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链
    ddddd,切入点(pointcut):指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点,例如,使用正则表达式。
    eeeee,引入(introductio)添加方法或字段到被通知的类。spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现ismodified接口,来简化缓存。
    fffff,目标对象(target object):包含连接点的对象,也被称作被通知或被代理的对象
    ggggg,AOP代理(aop proxy):aop框架创建的对象,包含通知。在spring中,AOP代理可以是JDK动态代理或CGLIB代理
    hhhhh,编织(weaving):组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。spring和其他纯java aop框架一样,在运行时完成织入。
    spring中AOP代理由spring的IOC容器负责生成,管理,其依赖关系也由IOC容器负责管理。
    =============================================================
    ==============================================================
    ====================================================================
    OOP(面向对象编程)针对业务处理过程的实体及其属性行为进行抽象封装,以获得更加清晰高效的逻辑单元划分
    -------是对对象的属性和行为的封装
    ===================================================================================
    ==========================================================================
    ==========================================================================
    插桩(测试,性能监控)
    在线热补丁
    sdk hooking(钩)
    ====================================================================
    ====================================================================
    ====================================================================
    AndFix使用方法
    引用
    dependencies{
    compile   'com.alipay.euler:andfix:0.33.'
    }
    初始化
    patchManager = new PatchManager(context);
    patchManager.init(appversion);
    加载补丁,尽量在application的oncreate方法中使用
    patchManager.loadPatch();
    应用补丁
    patchManager.addPatch(path);//path of the patch file that was downloaded

    项目中提供一个生成补丁(后缀为.apatch)的工具apkpatch
    ./apkpatch.sh -f new.apk -t old.apk -o ./ -k ../one.keystore -p *** -a one -e ***
    apkPatch工具解析
    apkpatch是一个jar包,也没有开源出来,我们可以用JD-GUI或者procyon来看下它的源码
    版本1.0.3
    首先找到main.class,位于com.euler.patch包下,找到main()方法
    public static void main(final String[] args) {
            .....
            //根据上面命令输入拿到参数        
           final ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore, password, alias, entry);
           apkPatch.doPatch();
      }
    apkpatch的dopatch方法
    public void doPatch() {
            try {
            //生成smali文件夹
                final File smaliDir = new File(this.out, "smali");
                if (!smaliDir.exists()) {
                    smaliDir.mkdir();
                }
                //新建diff.dex文件
                final File dexFile = new File(this.out, "diff.dex");
                //新建diff.apatch文件
                final File outFile = new File(this.out, "diff.apatch");
                //第一步,拿到两个apk文件对比,对比信息写入DiffInfo
                final DiffInfo info = new DexDiffer().diff(this.from, this.to);
                //第二步,将对比结果info写入.smali文件中,然后打包成dex文件
                this.classes = buildCode(smaliDir, dexFile, info);
                //第三步,将生成的dex文件写入jar包,并根据输入的签名信息进行签名,生成diff.apatch文件
                this.build(outFile, dexFile);
                //第四步,将diff.apatch文件重命名,结束
                this.release(this.out, dexFile, outFile);
            }
            catch (Exception e2) {
                e2.printStackTrace();
            }
        }

    以上可以简单描述为两步

    1. 对比apk文件,得到需要的信息
    2. 将结果打包为apatch文件

    对比apk文件

    DexDiffer().diff() 方法

    public DiffInfo diff(final File newFile, final File oldFile) throws IOException {
        //提取新apk的dex文件
            final DexBackedDexFile newDexFile = DexFileFactory.loadDexFile(newFile, 19, true);
            //提取旧apk的dex文件
            final DexBackedDexFile oldDexFile = DexFileFactory.loadDexFile(oldFile, 19, true);
            final DiffInfo info = DiffInfo.getInstance();
            boolean contains = false;
            for (final DexBackedClassDef newClazz : newDexFile.getClasses()) {
                final Set<? extends DexBackedClassDef> oldclasses = oldDexFile.getClasses();
                for (final DexBackedClassDef oldClazz : oldclasses) {
                	 //对比相同的方法,存储为修改的方法
                    if (newClazz.equals(oldClazz)) {
                    	 //对比class文件的变量
                        this.compareField(newClazz, oldClazz, info);
                        //对比class文件的方法
                        this.compareMethod(newClazz, oldClazz, info);
                        contains = true;
                        break;
                    }
                }
                if (!contains) {
                	 //否则是新增的方法
                    info.addAddedClasses(newClazz);
                }
            }
            //返回包含diff信息的DiffInfo对象
            return info;
        }

    其原理就是根据 dex diff 得到两个apk文件的差别信息。对比方法过程中对比两个 dex 文件中同时存在的方法,如果方法实现不同则 存储为修改过的方法 ;如果方法名不同, 存储为新增的方法,也就是说 AndFix支持增加新的方法 ,这一点已经测试证明。另外,在比较 Field 的时候有如下代码

    public void addAddedFields(DexBackedField field) {
      addedFields.add(field);
      throw new RuntimeException("can,t add new Field:" + 
        field.getName() + "(" + field.getType() + "), " + "in class :" + 
        field.getDefiningClass());
    }
    
    public void addModifiedFields(DexBackedField field) {
      modifiedFields.add(field);
      throw new RuntimeException("can,t modified Field:" + 
        field.getName() + "(" + field.getType() + "), " + "in class :" + 
        field.getDefiningClass());
    }
    

    也就是说 AndFix 不支持增加成员变量,但是支持在新增方法中增加的局部变量 。 也不支持修改成员变量 。已经测试证明这一点。

    还有一个地方要注意,就是提取 dex 文件的地方,在 DexFileFactory 类中

    public static DexBackedDexFile loadDexFile(File dexFile, int api, boolean experimental) throws IOException
      {
        return loadDexFile(dexFile, "classes.dex", new Opcodes(api, experimental));
      }
    

    可以看到,只提取出了 classes.dex 这个文件,所以源生工具并 不支持multidex ,如果使用了 multidex 方案,并且修复的类不在同一个 dex 文件中,那么补丁就不会生效。所以这里并不像作者在issue中提到的支持 multidex 那样,不过我们可以通过 JavaAssist 工具 修改 apkpatch 这个jar包,来达到支持multidex的目的 ,后续我们会讲到。

    将对比结果打包

    这一步我们重点关注拿到 DiffInfo 后将其存入 smali 文件的过程

    ApkPatch.buildCode() 方法

    private static Set<String> buildCode(final File smaliDir, final File dexFile, final DiffInfo info) throws IOException, RecognitionException, FileNotFoundException {
            final ClassFileNameHandler outFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");
            final ClassFileNameHandler inFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");
            final DexBuilder dexBuilder = DexBuilder.makeDexBuilder();
            for (final DexBackedClassDef classDef : list) {
                final String className = classDef.getType();
                baksmali.disassembleClass(classDef, outFileNameHandler, options);
                final File smaliFile = inFileNameHandler.getUniqueFilenameForClass(TypeGenUtil.newType(className));
                classes.add(TypeGenUtil.newType(className).substring(1, TypeGenUtil.newType(className).length() - 1).replace('/', '.'));
                SmaliMod.assembleSmaliFile(smaliFile, dexBuilder, true, true);
            }
            dexBuilder.writeTo(new FileDataStore(dexFile));
            return classes;
        }
    

    将上一步得到的 diff 信息写入 smali 文件,并且生成 diff.dex 文件。 smali 文件的命名以 _CF.smali 结尾,并且在修改的地方用自定义的 Annotation ( MethodReplace )标注,用于在替换之前查找修复的变量或方法,如下。

    .method private getUserProfile()V
        .locals 2
        .annotation runtime Lcom/alipay/euler/andfix/annotation/MethodReplace;
            clazz = "com.boohee.account.UserProfileActivity"
            method = "getUserProfile"
        .end annotation
    

    在打包生成的 diff.dex 文件中,反编译出来可以看到这段代码

    //生成的注解
    @MethodReplace(clazz="com.boohee.account.UserProfileActivity", method="onCreate")
      public void onCreate(Bundle paramBundle)
      {
        super.onCreate(paramBundle);
        getUserProfile();
        addPatch();
      }
    

    然后就是签名,打包,加密的流程,就不具体分析了。注意, apkPatch 在生成 .apatch 补丁文件的时候会加入签名信息,并且会进行加密操作,在应用补丁的时候会验证签名信息是否正确。

    打补丁原理

    Java层

    PatchManager.init() 方法

    public void init(String appVersion) {
        SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
            Context.MODE_PRIVATE);
        String ver = sp.getString(SP_VERSION, null);
        //根据版本号加载补丁文件,版本号不同清空缓存目录
        if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
          cleanPatch();
          sp.edit().putString(SP_VERSION, appVersion).commit();
        } else {
          initPatchs();
        }
      }
    
      private void initPatchs() {
        // 缓存目录data/data/package/file/apatch/会缓存补丁文件
        // 即使原目录被删除也可以打补丁
        File[] files = mPatchDir.listFiles();
        for (File file : files) {
          addPatch(file);
        }
      }
    

    addPatch 和 loadPatch() 方法

    public void addPatch(String path) throws IOException {
      ...
      FileUtil.copyFile(src, dest);// copy to patch's directory
      Patch patch = addPatch(dest);
      if (patch != null) {
        loadPatch(patch);
      }
    }
    
    private void loadPatch(Patch patch) {
      Set<String> patchNames = patch.getPatchNames();
      ClassLoader cl;
      List<String> classes;
      for (String patchName : patchNames) {
        if (mLoaders.containsKey("*")) {
          cl = mContext.getClassLoader();
        } else {
          cl = mLoaders.get(patchName);
        }
        if (cl != null) {
          classes = patch.getClasses(patchName);
          mAndFixManager.fix(patch.getFile(), cl, classes);
        }
      }
    }
    

    再看下 AndFixManager 的 fix() 方法

    ...
    //省略掉验证签名信息、安全检查的代码,安全方面做得很好
    ...
    
    private void fixClass(Class<?> clazz, ClassLoader classLoader) {
        ...
        for (Method method : methods) {
          //还记得对比过程中生成的Annotation注解吗
          //这里通过注解找到需要替换掉的方法
          methodReplace = method.getAnnotation(MethodReplace.class);
          if (methodReplace == null)
            continue;
          //标记的类
          clz = methodReplace.clazz();
          //需要替换的方法
          meth = methodReplace.method();
          if (!isEmpty(clz) && !isEmpty(meth)) {
            //所有找到的方法,循环替换
            replaceMethod(classLoader, clz, meth, method);
          }
        }
      }
      
      private static native void replaceMethod(Method dest, Method src);
      private static native void setFieldFlag(Field field);
    
      public static void addReplaceMethod(Method src, Method dest) {
        try {
          replaceMethod(src, dest);
          initFields(dest.getDeclaringClass());
        } catch (Throwable e) {
          Log.e(TAG, "addReplaceMethod", e);
        }
      }
    

    后面就是调用 native 层的方法,写在 jni 中,打包为 .so 文件供 java 层调用。

    总结一下, java 层的功能就是找到补丁文件,根据补丁中的注解找到将要替换的方法然后交给jni层去处理替换方法的操作。好了,继续往下看。

    Native层

    在 jni 的代码中支持 Dalvik 与 ART ,那么这是怎么区分的呢?在 AndFixManager 的构造方法中有这么一句

    mSupport = Compat.isSupport();
    
    public static synchronized boolean isSupport() {
      if (isChecked)
        return isSupport;
    
        isChecked = true;
        // not support alibaba's YunOs
        //SDK android 2.3 to android 6.0
        if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) {
          isSupport = true;
        }
      return isSupport;
    }
    

    AndFix 的`setUp()``方法

    public static boolean setup() {
      try {
        final String vmVersion = System.getProperty("java.vm.version");
        //判断是否是ART
        boolean isArt = vmVersion != null && vmVersion.startsWith("2");
        int apilevel = Build.VERSION.SDK_INT;
        //这里也是native方法
        return setup(isArt, apilevel);
      } catch (Exception e) {
        Log.e(TAG, "setup", e);
        return false;
      }
    }
    

    最后调用 setup(isArt, apilevel); 的 native 方法,在 andfix.cpp 中注册 jni 方法

    static JNINativeMethod gMethods[] = {
    /* name, signature, funcPtr */
    { "setup", "(ZI)Z", (void*) setup }, 
    { "replaceMethod",
        "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V",(void*) replaceMethod },
    { "setFieldFlag",
        "(Ljava/lang/reflect/Field;)V", (void*) setFieldFlag }, };
    

    native 实现

    static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart,
        jint apilevel) {
      isArt = isart;
      LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"),
          (int )apilevel);
      if (isArt) {
        return art_setup(env, (int) apilevel);
      } else {
        return dalvik_setup(env, (int) apilevel);
      }
    }
    
    static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
        jobject dest) {
      if (isArt) {
        art_replaceMethod(env, src, dest);
      } else {
        dalvik_replaceMethod(env, src, dest);
      }
    }
    

    根据上层传过来的 isArt 判断调用 Dalvik 还是 Art 的方法。

    以 Dalvik 为例,继续往下分析,代码在 dalvik_method_replace.cpp 中

    dalvik_setup 方法

    extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
        JNIEnv* env, int apilevel) {
      jni_env = env;
      void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
      if (dvm_hand) {
        ...
        //使用dlsym方法将dvmCallMethod_fnPtr函数指针指向libdvm.so中的		//dvmCallMethod方法,也就是说可以通过调用该函数指针执行其指向的方法
        //下面会用到dvmCallMethod_fnPtr
        dvmCallMethod_fnPtr = dvm_dlsym(dvm_hand,
          apilevel > 10 ?
          "_Z13dvmCallMethodP6ThreadPK6MethodP6ObjectP6JValuez" :
          "dvmCallMethod");
        ...
        }
    }
    

    替换方法的关键在于 native 层怎么影响内存里的java代码,我们知道 java 代码里将一个方法声明为 native 方法时,对此函数的调用就会到 native 世界里找,AndFix原理就是将一个不是native的方法修改成native方法,然后在 native 层进行替换,通过 dvmCallMethod_fnPtr 函数指针来调用 libdvm.so 中的 dvmCallMethod() 来加载替换后的新方法,达到替换方法的目的。 Jni 反射调用 java 方法时要用到一个 jmethodID 指针,这个指针在 Dalvik 里其实就是 Method 类,通过修改这个类的一些属性就可以实现在运行时将一个方法修改成 native 方法。

    看下 dalvik_replaceMethod(env, src, dest);

    extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
        JNIEnv* env, jobject src, jobject dest) {
      jobject clazz = env->CallObjectMethod(dest, jClassMethod);
      ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
          dvmThreadSelf_fnPtr(), clazz);
      //设置为初始化完毕
      clz->status = CLASS_INITIALIZED;
      //meth是将要被替换的方法
      Method* meth = (Method*) env->FromReflectedMethod(src);
      //target是新的方法
      Method* target = (Method*) env->FromReflectedMethod(dest);
      LOGD("dalvikMethod: %s", meth->name);
    
      meth->jniArgInfo = 0x80000000;
      //修改method的属性,将meth设置为native方法
      meth->accessFlags |= ACC_NATIVE;
    
      int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);
      if (!dvmIsStaticMethod(meth))
        argsSize++;
      meth->registersSize = meth->insSize = argsSize;
      //将新的方法信息保存到insns
      meth->insns = (void*) target;
      //绑定桥接函数,java方法的跳转函数
      meth->nativeFunc = dalvik_dispatcher;
    }
    
    static void dalvik_dispatcher(const u4* args, jvalue* pResult,
        const Method* method, void* self) {
        
      Method* meth = (Method*) method->insns;
      meth->accessFlags = meth->accessFlags | ACC_PUBLIC;
      if (!dvmIsStaticMethod(meth)) {
        Object* thisObj = (Object*) args[0];
        ClassObject* tmp = thisObj->clazz;
        thisObj->clazz = meth->clazz;
        argArray = boxMethodArgs(meth, args + 1);
        if (dvmCheckException_fnPtr(self))
          goto bail;
    
        dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
            dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,
            argArray);
    
        thisObj->clazz = tmp;
      } else {
        argArray = boxMethodArgs(meth, args);
        if (dvmCheckException_fnPtr(self))
          goto bail;
    
        dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
            dvmCreateReflectMethodObject_fnPtr(meth), &result, NULL,
            argArray);
      }
      bail: dvmReleaseTrackedAlloc_fnPtr((Object*) argArray, self);
    }
    

    通过 dalvik_dispatcher 这个跳转函数完成最后的替换工作,到这里就完成了两个方法的替换,有问题的方法就可以被修复后的方法取代。ART的替换方法就不讲了,原理上差别不大。

    总结

    AndFix热补丁原理就是在 native 动态替换方法 java 层的代码,通过 native 层hook java 层的代码。

    优点

    • 因为是动态的,所以不需要重启应用就可以生效
    • 支持ART与Dalvik
    • 与multidex方案相比,性能会有所提升(Multi Dex需要修改所有class的class_ispreverified标志位,导致运行时性能有所损失)
    • 支持新增加方法
    • 支持在新增方法中新增局部变量





    展开全文
  • AndFix下载

    2016-03-25 16:18:14
    AndFix下载
  • AndFix Module

    2019-03-22 10:37:59
    这是AndFix热修复项目中必须要用到的依赖项,需要导入到项目中进行开发
  • Andfix demo

    2016-03-21 15:48:50
    AndFix 热补丁修复 操作方法
  • AndFix框架

    2016-06-24 16:01:05
    从阿里AndFix框架提取的最新.so文件
  • AndFix AndFix is a solution to fix the bugs online instead of redistributing Android App. It is distributed as Android Library. Andfix is an acronym for "Android hot-fix". AndFix supports Android...
  • AndFix的使用

    2017-04-28 11:43:06
    AndFix的使用
  • 开源项目AndFix

    2021-04-04 20:49:38
    源码AndFix,里的另一 Android 开源项目 AndFix,可用于 App 进行在线 Bug 修复,无需 App 重启,支持 2.3 到最新的 6.0。
  • sunis andfix readme

    2016-07-07 16:39:09
    sunis andfix readme
  • AndFix.aar

    2016-06-02 19:19:17
    AndFix.aar
  • AndFix热修复

    2016-12-28 11:53:34
    AndFix 热修复学习
  • AndFix使用样例

    2016-07-28 10:18:17
    AndFix简单的使用案例
  • Andfix热修复Demo

    2017-11-30 16:37:45
    Andfix热修复Demo Andfix热修复DemoAndfix热修复Demo Andfix热修复Demo
  • andfix的demo

    2016-10-11 14:44:28
    andfix的热修复的例子
  • AndFix补丁生成工具

    2021-03-31 17:42:20
    AndFix补丁生成工具
  • AndFix集成项目

    2016-05-31 15:35:16
    AndFix集成,实现热修复
  • AndFix.zip

    2015-09-30 10:23:49
    AndFix 是阿里巴巴开源的 Android 应用热修复工具,帮助 Anroid 开发者修复应用的线上问题。Andfix 是 " And roid hot- fix " 的缩写。
  • AndFix工程项目

    2016-07-28 09:37:30
    AndFix工程项目,下载解压直接作为工程添加到Eclipse,然后在你的项目引入Android工程jar
  • AndFix热修复Demo

    2019-03-22 10:34:18
    android中阿里巴巴使用的AndFix热修复,比较简单易懂的一个实例
  • 热修复AndFix使用

    2018-10-19 16:35:38
    源码为andfix的使用,通过sd卡加载补丁文件修复bug,包含android stduio源码,apkpatch-1.0.3压缩包
  • andfix动态补丁

    2016-04-08 17:29:50
    android studio工程 自己参考其他博客和gitandfix后跑通的demo 博客地址...github地址:https://github.com/alibaba/AndFix 运行app-release.apk就能直接看到效果
  • AndFix源码分析

    2019-11-20 20:10:16
    Github地址镇楼:AndFix 首先介绍一下AndFix及其使用方法,然后根据使用流程对其内部实现进行分析。 AndFix介绍 首先我们看一下不同热修复框架的功能特性对比: 特性 AndFix Tinker/Amigo QQ空间 Robust/Aceso ...

    写在前面

    Github地址镇楼:AndFix

    首先介绍一下AndFix及其使用方法,然后根据使用流程对其内部实现进行分析。


    AndFix介绍

    首先我们看一下不同热修复框架的功能特性对比:

    特性AndFixTinker/AmigoQQ空间Robust/Aceso
    即时生效
    方法替换
    类替换
    类结构修改
    资源替换
    so替换
    支持gradle
    支持ART
    支持Android7.0

    AndFix的优势在于可以即时生效、接入和使用十分简单,但是缺点是只支持方法的替换,其他的替换不支持,且不支持gradle。Github上给出了它的适用范围:

    AndFix supports Android version from 2.3 to 7.0, both ARM and X86 architecture, both Dalvik and ART runtime, both 32bit and 64bit.

    AndFix支持Android版本从2.3到7.0,ARM和X86体系结构,Dalvik和ART运行时(32位和64位)。

    在这里插入图片描述

    源码中也对此进行了对应的限制:

    ### Compat.java
    
        // from android 2.3 to android 7.0
    	private static boolean isSupportSDKVersion() {
    		if (android.os.Build.VERSION.SDK_INT >= 8
    				&& android.os.Build.VERSION.SDK_INT <= 24) {
    			return true;
    		}
    		return false;
    	}
    

    在这里插入图片描述

    AndFix的修复流程图如下:

    在这里插入图片描述

    前面几步就是发现bug、定位bug、修改、测试等一系列操作,不涉及到SDK中的内容。当修复了bug,我们打出新的apk之后(后称修改后的为new.apk,有bug的为old.apk),通过apkpatch工具生成patch文件。

    在这里插入图片描述

    apkpatch的使用方法如下:

    在这里插入图片描述

    usage: apkpatch -f -t -o -k -p <***> -a -e <***>
    -a,–alias KeyStore.Entry别名
    -e,–epassword <***> KeyStore.Entry密码
    -f,–from new.apk文件路径
    -k,–keystore 签名文件路径
    -n,–name patch文件名
    -o,–out

    输出路径
    -p,–kpassword <***> keystore密码
    -t,–to old.apk文件路径


    AndFix使用

    1.Initialize PatchManager
    patchManager = new PatchManager(context);
    patchManager.init(appversion);//current version
    
    2.Load patch
    patchManager.loadPatch();
    

    You should load patch as early as possible, generally, in the initialization phase of your application(such as Application.onCreate()).

    3.Add patch
    patchManager.addPatch(path);//path of the patch file that was downloaded
    

    When a new patch file has been downloaded, it will become effective immediately by addPatch.

    上面是Github上官方给出的加载流程,即AndFix提供的是一套生成patch、合并patch、应用patch的流程,在你自己的项目中使用时,还需要你自己去实现一套类似与更新检测下载的流程。


    AndFix源码分析

    首先给出一个源码的关系图,有一个整体的认识:

    在这里插入图片描述

    源码分析按照官方给出的使用流程的顺序来进行阅读

    1.Initialize PatchManager
    patchManager = new PatchManager(context);
    patchManager.init(appversion);//current version
    

    在这里我们看到了第一个关键类,PatchManager。从上面的使用方法中可以看到,PatchManager提供的入口类,构造函数实现:

        public PatchManager(Context context) {
    		mContext = context;
    		mAndFixManager = new AndFixManager(mContext);
    		mPatchDir = new File(mContext.getFilesDir(), DIR);
    		mPatchs = new ConcurrentSkipListSet<Patch>();
    		mLoaders = new ConcurrentHashMap<String, ClassLoader>();
    	}
    

    构造函数中初始化了成员变量:

    AndFixManager负责调用fix、replaceMethod等替换方法的关键代码
    PatchDir是patch文件的存放路径
    Patchs是读取出的所有patch文件
    Loaders是指定的ClassLoader,默认使用context.getClassLoader()

    接下来看一下init中的实现:

    	public void init(String appVersion) {
    		if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
    			Log.e(TAG, "patch dir create error.");
    			return;
    		} else if (!mPatchDir.isDirectory()) {// not directory
    			mPatchDir.delete();
    			return;
    		}
    		SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
    				Context.MODE_PRIVATE);
    		String ver = sp.getString(SP_VERSION, null);
    		if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
    			cleanPatch();
    			sp.edit().putString(SP_VERSION, appVersion).commit();
    		} else {
    			initPatchs();
    		}
    	}
    

    init中首先判断apatch文件夹是否存在,然后从SharedPreferences中取出保存的版本号进行对比:如果版本号不存在或是不相同,则调用cleanPatch清理掉所有patch文件并更新版本号;相同情况下才会进行patch是否可以应用的判断initPatchs。我们先来看清理patch的代码:

        # PatchManager
        
        private void cleanPatch() {
    		File[] files = mPatchDir.listFiles();
    		for (File file : files) {
    			mAndFixManager.removeOptFile(file);
    			if (!FileUtil.deleteFile(file)) {
    				Log.e(TAG, file.getName() + " delete error.");
    			}
    		}
    	}
    	
    	# AndFixManager
    	
    	public synchronized void removeOptFile(File file) {
    		File optfile = new File(mOptDir, file.getName());
    		if (optfile.exists() && !optfile.delete()) {
    			Log.e(TAG, optfile.getName() + " delete error.");
    		}
    	}
    

    这里在清理apatch文件夹下patch文件的同时,也清理了apatch_opt文件下的文件。apatch_opt文件夹下的文件在后面会讲到,这里简单提一下:首先,AndFix的热修复方案并不是从根本上改变了我们安装程序的字节码,而是在程序运行时,将原来指向old.apk中有bug的方法的指针,指向new.apk中新方法。这也是AndFix有明确的版本大小判断的原因,因为当前只做了对这些版本的虚拟机的方案。其次,关于opt文件夹下生成文件的原因,就是.apatch格式是自定义的格式,在后面的方法中可以看到,第一次执行时需要转换成DexFile才能从中获取到类的各种信息,而这个转换过程也会占用很多的资源,所以直接将转换好的文件存放在一个新的文件夹中,以空间换时间,故清理时也要同步清理干净。

    接下来我们看一下初始化patch文件的操作initPatchs:

        private static final String SUFFIX = ".apatch";
        
        private void initPatchs() {
    		File[] files = mPatchDir.listFiles();
    		for (File file : files) {
    			addPatch(file);
    		}
    	}
    	
    	private Patch addPatch(File file) {
    		Patch patch = null;
    		if (file.getName().endsWith(SUFFIX)) {
    			try {
    				patch = new Patch(file);
    				mPatchs.add(patch);
    			} catch (IOException e) {
    				Log.e(TAG, "addPatch", e);
    			}
    		}
    		return patch;
    	}
    

    这里的逻辑很简单,就是加载路径下的所有patch文件,存放在Set中。至此,初始化的操作就完成了。

    2.Load patch
        patchManager.loadPatch();
        
        # PatchManager
    
        /**
    	 * load patch,call when application start
    	 */
        public void loadPatch() {
    		mLoaders.put("*", mContext.getClassLoader());// wildcard
    		Set<String> patchNames;
    		List<String> classes;
    		for (Patch patch : mPatchs) {
    			patchNames = patch.getPatchNames();
    			for (String patchName : patchNames) {
    				classes = patch.getClasses(patchName);
    				mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
    						classes);
    			}
    		}
    	}
    	
    	# Patch
    	
    	public Set<String> getPatchNames() {
    		return mClassesMap.keySet();
    	}
    
    	public List<String> getClasses(String patchName) {
    		return mClassesMap.get(patchName);
    	}
    

    loadPatch无参函数对应的是当app启动时调用的,所以ClassLoader使用的是mContext.getClassLoader()。接下来遍历mPatchs,Patch中的mClassesMap保存了需要进行修复的class名,对每个class调用mAndFixManager.fix。我们先来看一下mClassesMap的生成:

        private static final String ENTRY_NAME = "META-INF/PATCH.MF";
    	private static final String CLASSES = "-Classes";
    	private static final String PATCH_CLASSES = "Patch-Classes";
    	private static final String CREATED_TIME = "Created-Time";
    	private static final String PATCH_NAME = "Patch-Name";
    
        public Patch(File file) throws IOException {
    		mFile = file;
    		init();
    	}
    
    	@SuppressWarnings("deprecation")
    	private void init() throws IOException {
    		JarFile jarFile = null;
    		InputStream inputStream = null;
    		try {
    			jarFile = new JarFile(mFile);
    			JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
    			inputStream = jarFile.getInputStream(entry);
    			Manifest manifest = new Manifest(inputStream);
    			Attributes main = manifest.getMainAttributes();
    			mName = main.getValue(PATCH_NAME);
    			mTime = new Date(main.getValue(CREATED_TIME));
    
    			mClassesMap = new HashMap<String, List<String>>();
    			Attributes.Name attrName;
    			String name;
    			List<String> strings;
    			for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
    				attrName = (Attributes.Name) it.next();
    				name = attrName.toString();
    				if (name.endsWith(CLASSES)) {
    					strings = Arrays.asList(main.getValue(attrName).split(","));
    					if (name.equalsIgnoreCase(PATCH_CLASSES)) {
    						mClassesMap.put(mName, strings);
    					} else {
    						mClassesMap.put(
    								name.trim().substring(0, name.length() - 8),// remove "-Classes"
    								strings);
    					}
    				}
    			}
    		} finally {
    			if (jarFile != null) {
    				jarFile.close();
    			}
    			if (inputStream != null) {
    				inputStream.close();
    			}
    		}
    
    	}
    

    Patch实例化时,将文件按照固定格式进行解析,取出所有结尾为“-Classes”的attrName放入mClassesMap中。接下来我们看一下AndFixManager中的fix方法,由于方法过长,已经将注释加到了代码之中:

        public synchronized void fix(File file, ClassLoader classLoader,
    			List<String> classes) {
    		// SecurityChecker中检测的版本是否支持
    		if (!mSupport) {
    			return;
    		}
    
    		// 检测签名,防止恶意植入
    		if (!mSecurityChecker.verifyApk(file)) {// security check fail
    			return;
    		}
    
    		try {
    			// 检测opt文件夹下对应的文件:如果存在,检测指纹(第一次生成时生成了对应的指纹信息,保存在了sp中)
    			File optfile = new File(mOptDir, file.getName());
    			boolean saveFingerprint = true;
    			if (optfile.exists()) {
    				// need to verify fingerprint when the optimize file exist,
    				// prevent someone attack on jailbreak device with
    				// Vulnerability-Parasyte.
    				// btw:exaggerated android Vulnerability-Parasyte
    				// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
    				if (mSecurityChecker.verifyOpt(optfile)) {
    					saveFingerprint = false;
    				} else if (!optfile.delete()) {
    					return;
    				}
    			}
    
    			// 注1,见后面
    			final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
    					optfile.getAbsolutePath(), Context.MODE_PRIVATE);
    
    			// 如果不是已存在的文件,则需要保存指纹信息,用于下次使用时校验
    			if (saveFingerprint) {
    				mSecurityChecker.saveOptSig(optfile);
    			}
    
    			// findClass中增加了对包名的检测
    			ClassLoader patchClassLoader = new ClassLoader(classLoader) {
    				@Override
    				protected Class<?> findClass(String className)
    						throws ClassNotFoundException {
    					Class<?> clazz = dexFile.loadClass(className, this);
    					if (clazz == null
    							&& className.startsWith("com.alipay.euler.andfix")) {
    						return Class.forName(className);// annotation’s class
    														// not found
    					}
    					if (clazz == null) {
    						throw new ClassNotFoundException(className);
    					}
    					return clazz;
    				}
    			};
    
    			// 注2,见后面
    			Enumeration<String> entrys = dexFile.entries();
    			Class<?> clazz = null;
    			while (entrys.hasMoreElements()) {
    				String entry = entrys.nextElement();
    				if (classes != null && !classes.contains(entry)) {
    					continue;// skip, not need fix
    				}
    
    				// 此时classes中含有该entry,加载这个class
    				clazz = dexFile.loadClass(entry, patchClassLoader);
    				if (clazz != null) {
    					// 调用fixClass修复
    					fixClass(clazz, classLoader);
    				}
    			}
    		} catch (IOException e) {
    			Log.e(TAG, "pacth", e);
    		}
    	}
    	
    		
    	// 注1:附上loadDex方法的注释,在loadDex方法中实现了将dex文件持久化及服用的操作
    	
    	# DexFile
    	/**
         * Open a DEX file, specifying the file in which the optimized DEX
         * data should be written.  If the optimized form exists and appears
         * to be current, it will be used; if not, the VM will attempt to
         * regenerate it.
         *
         * This is intended for use by applications that wish to download
         * and execute DEX files outside the usual application installation
         * mechanism.  This function should not be called directly by an
         * application; instead, use a class loader such as
         * dalvik.system.DexClassLoader.
         *
         * @param sourcePathName
         *  Jar or APK file with "classes.dex".  (May expand this to include
         *  "raw DEX" in the future.)
         * @param outputPathName
         *  File that will hold the optimized form of the DEX data.
         * @param flags
         *  Enable optional features.  (Currently none defined.)
         * @return
         *  A new or previously-opened DexFile.
         * @throws IOException
         *  If unable to open the source or output file.
         */
        static public DexFile loadDex(String sourcePathName, String outputPathName,
            int flags) throws IOException
    	
    	
    	// 注2
    	/**
         * Enumerate the names of the classes in this DEX file.
         * 
         * @return an enumeration of names of classes contained in the DEX file, in
         *         the usual internal form (like "java/lang/String").
         */
        public Enumeration<String> entries() {
            return new DFEnum(this);
        }
        
    	/*
         * Helper class.
         */
        private class DFEnum implements Enumeration<String> {
            private int mIndex;
            private String[] mNameList;
    
            DFEnum(DexFile df) {
                mIndex = 0;
                mNameList = getClassNameList(mCookie);
            }
    
            public boolean hasMoreElements() {
                return (mIndex < mNameList.length);
            }
    
            public String nextElement() {
                return mNameList[mIndex++];
            }
        }
    

    简单总结一下上面的代码,我们先检测文件的安全性(防止恶意修改),然后根据class名找到要修复的Class对象,调用fixClass方法。接下来我们来看fixClass方法:

        private void fixClass(Class<?> clazz, ClassLoader classLoader) {
    		// 获取到所有方法
    		Method[] methods = clazz.getDeclaredMethods();
    		MethodReplace methodReplace;
    		String clz;
    		String meth;
    		for (Method method : methods) {
    			// 在生成patch文件时,在对应方法上添加了注解{@MethodReplace}
    			methodReplace = method.getAnnotation(MethodReplace.class);
    			if (methodReplace == null)
    				continue;
    			clz = methodReplace.clazz();
    			meth = methodReplace.method();
    			if (!isEmpty(clz) && !isEmpty(meth)) {
    				// 找到了需要替换的方法,进行替换
    				replaceMethod(classLoader, clz, meth, method);
    			}
    		}
    	}
    	
    	private void replaceMethod(ClassLoader classLoader, String clz,
    			String meth, Method method) {
    		try {
    			String key = clz + "@" + classLoader.toString();
    			Class<?> clazz = mFixedClass.get(key);
    			if (clazz == null) {// class not load
    				Class<?> clzz = classLoader.loadClass(clz);
    				// initialize target class
    				clazz = AndFix.initTargetClass(clzz);
    			}
    			if (clazz != null) {// initialize class OK
    				mFixedClass.put(key, clazz);
    				Method src = clazz.getDeclaredMethod(meth,
    						method.getParameterTypes());
    				AndFix.addReplaceMethod(src, method);
    			}
    		} catch (Exception e) {
    			Log.e(TAG, "replaceMethod", e);
    		}
    	}
    

    前面部分增加了注释不多说了,后面部分其实就是调用了AndFix中的initTargetClass和addReplaceMethod方法。我们先来看initTargetClass,该方法的目的时将方法域修改为public:

        public static Class<?> initTargetClass(Class<?> clazz) {
    		try {
    			Class<?> targetClazz = Class.forName(clazz.getName(), true,
    					clazz.getClassLoader());
                // 核心代码
    			initFields(targetClazz);
    			return targetClazz;
    		} catch (Exception e) {
    			Log.e(TAG, "initTargetClass", e);
    		}
    		return null;
    	}
    	
    	private static void initFields(Class<?> clazz) {
    		Field[] srcFields = clazz.getDeclaredFields();
    		for (Field srcField : srcFields) {
    			Log.d(TAG, "modify " + clazz.getName() + "." + srcField.getName()
    					+ " flag:");
    			setFieldFlag(srcField);
    		}
    	}
    	
    	private static native void setFieldFlag(Field field);
    

    经过一步步的调用,最终调用了这个native方法,我们进去看一下:

    static void setFieldFlag(JNIEnv* env, jclass clazz, jobject field) {
    	if (isArt) {
    		art_setFieldFlag(env, field);
    	} else {
    		dalvik_setFieldFlag(env, field);
    	}
    }
    

    根据虚拟机不同,调用不同方法,我们来看一下art的

    extern void __attribute__ ((visibility ("hidden"))) art_setFieldFlag(
    		JNIEnv* env, jobject field) {
        if (apilevel > 23) {
            setFieldFlag_7_0(env, field);
        } else if (apilevel > 22) {
    		setFieldFlag_6_0(env, field);
    	} else if (apilevel > 21) {
    		setFieldFlag_5_1(env, field);
    	} else  if (apilevel > 19) {
    		setFieldFlag_5_0(env, field);
        }else{
            setFieldFlag_4_4(env, field);
        }
    }
    
    // 以4.4为例
    void setFieldFlag_4_4(JNIEnv* env, jobject field) {
    	art::mirror::ArtField* artField =
    			(art::mirror::ArtField*) env->FromReflectedField(field);
    	artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
    	LOGD("setFieldFlag_4_4: %d ", artField->access_flags_);
    }
    

    至此就将方法域修改为了public。接下来我们看一下addReplaceMethod方法:

        public static void addReplaceMethod(Method src, Method dest) {
    		try {
    			replaceMethod(src, dest);
    			initFields(dest.getDeclaringClass());
    		} catch (Throwable e) {
    			Log.e(TAG, "addReplaceMethod", e);
    		}
    	}
    	
    	private static native void replaceMethod(Method dest, Method src);
    

    到这个native方法中看一看:

    // 我们还是以art为例
    static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
    		jobject dest) {
    	if (isArt) {
    		art_replaceMethod(env, src, dest);
    	} else {
    		dalvik_replaceMethod(env, src, dest);
    	}
    }
    
    // 我们还是以4.4为例
    extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
    		JNIEnv* env, jobject src, jobject dest) {
        if (apilevel > 23) {
            replace_7_0(env, src, dest);
        } else if (apilevel > 22) {
    		replace_6_0(env, src, dest);
    	} else if (apilevel > 21) {
    		replace_5_1(env, src, dest);
    	} else if (apilevel > 19) {
    		replace_5_0(env, src, dest);
        }else{
            replace_4_4(env, src, dest);
        }
    }
    
    void replace_4_4(JNIEnv* env, jobject src, jobject dest) {
        // old.apk中对应的方法
    	art::mirror::ArtMethod* smeth =
    			(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
    
        // new.apk中对应的方法
    	art::mirror::ArtMethod* dmeth =
    			(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
    
    	dmeth->declaring_class_->class_loader_ =
    			smeth->declaring_class_->class_loader_; //for plugin classloader
    	dmeth->declaring_class_->clinit_thread_id_ =
    			smeth->declaring_class_->clinit_thread_id_;
    	dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
    	//for reflection invoke
    	reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
    
        // 指针指向新方法
    	smeth->declaring_class_ = dmeth->declaring_class_;
        smeth->dex_cache_initialized_static_storage_ = dmeth->dex_cache_initialized_static_storage_;
        smeth->access_flags_ = dmeth->access_flags_  | 0x0001;
        smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
        smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
        smeth->dex_cache_strings_ = dmeth->dex_cache_strings_;
        smeth->code_item_offset_ = dmeth->code_item_offset_;
        smeth->core_spill_mask_ = dmeth->core_spill_mask_;
        smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
        smeth->method_dex_index_ = dmeth->method_dex_index_;
        smeth->mapping_table_ = dmeth->mapping_table_;
        smeth->method_index_ = dmeth->method_index_;
        smeth->gc_map_ = dmeth->gc_map_;
        smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
        smeth->native_method_ = dmeth->native_method_;
        smeth->vmap_table_ = dmeth->vmap_table_;
    
        smeth->entry_point_from_compiled_code_ = dmeth->entry_point_from_compiled_code_;
        
        smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
        
        smeth->method_index_ = dmeth->method_index_;
    
    	LOGD("replace_4_4: %d , %d", smeth->entry_point_from_compiled_code_,
    			dmeth->entry_point_from_compiled_code_);
    
    }
    

    最后这部分代码很长,可以看出是指针指向新方法,具体的变量我不是很懂,留给大佬们去解释吧~至此,loadPatch的流程就结束了。

    3.Add patch
    patchManager.addPatch(path);//path of the patch file that was downloaded
    
        public void addPatch(String path) throws IOException {
    		File src = new File(path);
    		File dest = new File(mPatchDir, src.getName());
    		if(!src.exists()){
    			throw new FileNotFoundException(path);
    		}
    		if (dest.exists()) {
    			Log.d(TAG, "patch [" + path + "] has be loaded.");
    			return;
    		}
    		FileUtil.copyFile(src, dest);// copy to patch's directory
    		Patch patch = addPatch(dest);
    		if (patch != null) {
    			loadPatch(patch);
    		}
    	}
    
        private void loadPatch(Patch patch) {
    		Set<String> patchNames = patch.getPatchNames();
    		ClassLoader cl;
    		List<String> classes;
    		for (String patchName : patchNames) {
    			if (mLoaders.containsKey("*")) {
    				cl = mContext.getClassLoader();
    			} else {
    				cl = mLoaders.get(patchName);
    			}
    			if (cl != null) {
    				classes = patch.getClasses(patchName);
    				mAndFixManager.fix(patch.getFile(), cl, classes);
    			}
    		}
    	}
    

    这里可以看到,addPatch最后调用了loadPatch方法,流程与上一步一样,这里就不多解释了。

    至此,AndFix的源码分析就结束了,如果出现了错误,麻烦大佬们指出~


    结语

    If you like it, it’s written by Johnny Deng.
    If not,then I don’t know who wrote it.

    展开全文
  • AndFix 了解

    2017-07-18 14:00:45
    compile 'com.alipay.euler:andfix:0.4.1@aar' 使用工具对比原始apk 生成.apatch 补丁包后,移动端下载下来,通过PatchManager.loadPath(File ) 安装此补丁包,也可以通过 PatchManager.removeAllPa

    目前开源最新版本是 0.5.0 , 问题还是很多,于是使用的0.4.1的 稳定版。

        compile 'com.alipay.euler:andfix:0.4.1@aar'
    


    使用工具对比原始apk 生成.apatch 补丁包后,移动端下载下来,通过PatchManager.loadPath(File ) 安装此补丁包,也可以通过 PatchManager.removeAllPatch()删除所有历史补丁包


    局限性:

    1   无法修改so
    2   无法修改布局
    3   5.0以上 新增类、方法有时会有问题  尽量避免
    4   addPatch 是及时生效  removeAllPatch 是下次启动生效
    6   修改的方法如果方法内部实现为空,会出现修复失败的情况
    7   aidl有更新的话补丁包生成失败
    8   二次对一个类做修复操作会导致修复的方法无法调用该类中的其他方法

    遇到的问题:

    第一次修改一个方法时,是使用原始class 来修改 MainActivity.a() -> MainActivity_CF.a();

    第二次修改此方法,方法内部新增调用class 中其他方法时 会 出现异常 MainActivity_CF.b() can not ..... 重启后正常加载


    不知如何解决

    因此若是要使用andrix的话,只能手动控制 只会加载一次 patch。

    展开全文
  • AndFix 使用详解

    2019-11-26 15:53:02
    AndFix基本介绍 ​ 已经好几年没有维护了,阿里出了一个收费的。这个已经被放弃了。这里只是简单的介绍一下用法。如果不想看的,可以直接跳过 ​ AndFix是一个在线修复bug的解决方案,而不是重新发布Android App...
  • 作为阿里巴巴开源的 Android APP——热修复工具AndFix ,帮助 Anroid 开发者修复应用的线上问题。Andfix 是 “Android hot-fix” 的缩写。1.什么是AndFixAndFix是阿里巴巴出的一个专门针对Android的热修复框架,那...
  • andfix使用

    2016-05-08 16:34:00
    1.andfix简介 AndFix是一个AndroidApp的在线热补丁框架。使用此框架,我们能够在不重复发版的情况下,在线修改App中的Bug。AndFix就是 “Android Hot-Fix”的缩写。就目前来说,AndFix支持Android 2.3到6.0版本,...
  • 热修复框架AndFix解析

    2016-11-10 10:24:07
    热修复框架AndFix
  • Andfix学习记录

    千次阅读 2017-01-11 10:41:40
    概述篇AndFix,全称是Android hot-fix。是阿里开源的一个热补丁框架,允许APP在不重新发布版本的情况下修复线上的bug。支持Android 2.3 到 7.0,并且支持arm 与 X86系统架构的设备。完美支持Dalvik与ART的Runtime,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,539
精华内容 1,415
关键字:

andfix