美团Robust热修复接入实践

Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明

编译打包阶段自动为每个class都增加了一个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。

优点:兼容性高,成功率高,不需要重启,支持混淆、方法和类
缺点:侵入式,增加 apk 体积

GitHub:https://github.com/Meituan-Dianping/Robust

美团Robust热修复原理

依赖

(1)在项目build.gradle中添加插件依赖

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.meituan.robust:gradle-plugin:0.4.99'
        classpath 'com.meituan.robust:auto-patch-plugin:0.4.99'
   }
}

(2)在app模块build.gradle中添加依赖

apply plugin: 'com.android.application'
//please uncomment fellow line before you build a patch
//只有需要patch
//apply plugin: 'auto-patch-plugin'
apply plugin: 'robust'

implementation 'com.meituan.robust:robust:0.4.99'

(3)配置app/robust.xml

在app目录下配置robust.xml文件,参考:app/robust.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <switch>
        <!--true代表打开Robust,请注意即使这个值为true,Robust也默认只在Release模式下开启-->
        <!--false代表关闭Robust,无论是Debug还是Release模式都不会运行robust-->
        <turnOnRobust>true</turnOnRobust>
        <!--<turnOnRobust>false</turnOnRobust>-->

        <!--是否开启手动模式,手动模式会去寻找配置项patchPackname包名下的所有类,自动的处理混淆,然后把patchPackname包名下的所有类制作成补丁-->
        <!--这个开关只是把配置项patchPackname包名下的所有类制作成补丁,适用于特殊情况,一般不会遇到-->
        <!--<manual>true</manual>-->
        <manual>false</manual>

        <!--是否强制插入插入代码,Robust默认在debug模式下是关闭的,开启这个选项为true会在debug下插入代码-->
        <!--但是当配置项turnOnRobust是false时,这个配置项不会生效-->
        <!--<forceInsert>true</forceInsert>-->
        <forceInsert>true</forceInsert>

        <!--是否捕获补丁中所有异常,建议上线的时候这个开关的值为true,测试的时候为false-->
        <catchReflectException>true</catchReflectException>
        <!--<catchReflectException>false</catchReflectException>-->

        <!--是否在补丁加上log,建议上线的时候这个开关的值为false,测试的时候为true-->
        <!--<patchLog>true</patchLog>-->
        <patchLog>false</patchLog>

        <!--项目是否支持progaurd-->
        <!--<proguard>true</proguard>-->
        <proguard>false</proguard>

        <!--项目是否支持ASM进行插桩,默认使用ASM,推荐使用ASM,Javaassist在容易和其他字节码工具相互干扰-->
        <useAsm>true</useAsm>
        <!--<useAsm>false</useAsm>-->

        <!--针对Java8级别的Lambda表达式,编译为private级别的javac函数,此时由开发者决定是否进行插桩处理-->
        <forceInsertLambda>true</forceInsertLambda>
        <!--<forceInsertLambda>false</forceInsertLambda>-->
    </switch>

    <!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码-->
    <!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名,
    这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的-->
    <packname name="hotfixPackage">
        <name>me.yezhou</name>
        <name>cn.appblog</name>
    </packname>

    <!--不需要Robust插入代码的包名,Robust库不需要插入代码,如下的配置项请保留,还可以根据各个APP的情况执行添加-->
    <exceptPackname name="exceptPackage">
        <name>com.meituan.robust</name>
        <name>com.meituan.extension</name>
    </exceptPackname>

    <!--补丁的包名,请保持和类PatchManipulateImp中fetchPatchList方法中设置的补丁类名保持一致(setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl")),
    各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是如下的配置项,类名必须是:PatchesInfoImpl-->
    <patchPackname name="patchPackname">
        <name>com.meituan.robust.patch</name>
    </patchPackname>

    <!--自动化补丁中,不需要反射处理的类,这个配置项慎重选择-->
    <noNeedReflectClass name="classes no need to reflect">

    </noNeedReflectClass>
</resources>

优势

  • 支持Android 2.3-10版本
  • 高兼容性、高稳定性,修复成功率高达99.9%
  • 补丁实时生效,不需要重新启动
  • 支持方法级别的修复,包括静态方法
  • 支持增加方法和类
  • 支持ProGuard的混淆、内联、优化等操作

需要保存打包时生成的mapping文件(开启混淆时才有)以及build/outputs/robust/methodsMap.robust文件

注意 Gradle 3.6及以上版本默认启用R8,会将插入的ChangeQuickRedirect变量优化掉,需要在混淆文件proguard-rules.pro中加入以下代码

-keepclassmembers class **{ public static com.meituan.robust.ChangeQuickRedirect *; }

AutoPatch

Robust补丁自动化,为Robust自动生成补丁,使用者只需要提交修改完bug后的代码,运行和线上apk打包同样的gradle命令即可,会在项目的app/build/outputs/robust目录下生成补丁。更多自动化补丁信息请参考:Android热更新方案Robust开源,新增自动化补丁工具

使用方法

(1)配置配置app/robust.xml,编译原工程

..\gradlew clean assemble --stacktrace
..\gradlew clean assembleDebug --stacktrace
..\gradlew clean assembleRelease --stacktrace

可以看到Robust为相关类和方法插入代码日志:

注:巨坑,如果方法中没有任何实现代码,只有一个return语句,那么不会为其插入代码

> Task :app:transformClassesWithRobustForRelease
================robust start================
read all class file cost 0.208 second
===robust print id start===
key is   me.yezhou.robust.MainActivity.onCreate(android.os.Bundle)  value is    1
key is   me.yezhou.robust.MainActivity.runRobust()  value is    2
key is   me.yezhou.robust.MainActivity.clickMe(android.view.View)  value is    3
key is   me.yezhou.robust.MainActivity.patch(android.view.View)  value is    4
key is   me.yezhou.robust.MainActivity.isGrantSDCardReadPermission()  value is    5
key is   me.yezhou.robust.MainActivity.requestPermission()  value is    6
key is   me.yezhou.robust.MainActivity.onRequestPermissionsResult(int,java.lang.String[],int[])  value is    7
key is   me.yezhou.robust.MainActivity.handlePermissionResult()  value is    8
key is   me.yezhou.robust.MainActivity.jump(android.view.View)  value is    9
key is   me.yezhou.robust.PatchManipulateImpl.fetchPatchList(android.content.Context)  value is    10
key is   me.yezhou.robust.PatchManipulateImpl.verifyPatch(android.content.Context,com.meituan.robust.Patch)  value is    11
key is   me.yezhou.robust.PatchManipulateImpl.copy(java.lang.String,java.lang.String)  value is    12
key is   me.yezhou.robust.PermissionUtils.checkSelfPermission(android.content.Context,java.lang.String)  value is    13
key is   me.yezhou.robust.PermissionUtils.isGrantSDCardReadPermission(android.content.Context)  value is    14
key is   me.yezhou.robust.PermissionUtils.requestSDCardReadPermission(android.app.Activity,int)  value is    15
key is   me.yezhou.robust.RobustCallBackSample.onPatchListFetched(boolean,boolean,java.util.List)  value is    16
key is   me.yezhou.robust.RobustCallBackSample.onPatchFetched(boolean,boolean,com.meituan.robust.Patch)  value is    17
key is   me.yezhou.robust.RobustCallBackSample.onPatchApplied(boolean,com.meituan.robust.Patch)  value is    18
key is   me.yezhou.robust.RobustCallBackSample.logNotify(java.lang.String,java.lang.String)  value is    19
key is   me.yezhou.robust.RobustCallBackSample.exceptionNotify(java.lang.Throwable,java.lang.String)  value is    20
key is   me.yezhou.robust.SecondActivity.onCreate(android.os.Bundle)  value is    21
key is   me.yezhou.robust.SecondActivity.getTextInfo()  value is    22
key is   me.yezhou.robust.SecondActivity.onCreateView(java.lang.String,android.content.Context,android.util.AttributeSet)  value is    23
key is   me.yezhou.robust.SecondActivity.onClick(android.view.View)  value is    24
key is   me.yezhou.robust.SecondActivity.getReflectField(java.lang.String,java.lang.Object)  value is    25
key is   me.yezhou.robust.SecondActivity.getFieldValue(java.lang.String,java.lang.Object)  value is    26
key is   me.yezhou.robust.SecondActivity.printLog(java.lang.String,java.lang.String[][])  value is    27
key is   me.yezhou.robust.SecondActivity.lambda$onCreate$0(android.view.View)  value is    28
===robust print id end===
robust cost 4.316 second
================robust   end================

(2)将保存下来的mapping文件和methodsMap.robust文件放在app/robust/文件夹下

(3)开启apply plugin: 'auto-patch-plugin'

使用插件时,需要把auto-patch-plugin放置在com.android.application插件之后,其余插件之前

apply plugin: 'com.android.application'
apply plugin: 'auto-patch-plugin'

(4)修改代码,在改动的方法上面添加@Modify注解,对于Lambda表达式请在修改的方法里面调用RobustModify.modify()方法

@Modify
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
}
//或者是被修改的方法里面调用RobustModify.modify()方法
protected void onCreate(Bundle savedInstanceState) {
    RobustModify.modify();
    super.onCreate(savedInstanceState);
}

新增的方法和字段使用@Add注解

//增加方法
@Add
public String getString() {
    return "Robust";
}
//增加类
@Add
public class NewAddCLass {
    public static String get() {
       return "robust";
    }
}

(5)运行和生成线上apk同样的命令,即可生成补丁,补丁目录app/build/outputs/robust/patch.jar

(6)补丁制作成功后会停止构建apk,出现类似于如下的提示,表示补丁生成成功

Caused by: java.lang.RuntimeException: auto patch end successfully
        at robust.gradle.plugin.AutoPatchTransform.transform(AutoPatchTransform.groovy:103)
        at com.android.build.api.transform.Transform.transform(Transform.java:314)
        at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:284)
        at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:247)
        at com.android.builder.profile.ThreadRecorder.record(ThreadRecorder.java:106)

从日志中还可以看到新增以及修改的方法:

> Task :app:transformClassesWithAutoPatchTransformForDebug
================autoPatch start================
autopatch read all class file cost 0.307 second
check all class cost 1.155 second, class count: 2872
new add methods  list is
key is   me.yezhou.robust.MainActivity.getString()
key is   me.yezhou.robust.SecondActivity.getArray()

new add classes list is

 patchMethodSignatureSet is printed below
key is   me.yezhou.robust.SecondActivity.getTextInfo()
key is   me.yezhou.robust.MainActivity.clickMe(android.view.View)
key is   me.yezhou.robust.MainActivity.onCreate(android.os.Bundle)
key is   me.yezhou.robust.SecondActivity.lambda$onCreate$0(android.view.View)

补丁应用

(1)将补丁文件拷贝或通过网络下载到手机目录/sdcard/robust

adb push ~/Desktop/code/robust/app/build/outputs/robust/patch.jar /sdcard/robust/patch.jar

(2)对patch.jar打补丁,方法详见代码

patch日志:

me.yezhou.robust W/robust: robustApkHash :91c30a260205103615c83f95e0343475
me.yezhou.robust D/robust:  patchManipulate list size is 1
me.yezhou.robust D/robust: patch patch_info_name:com.meituan.robust.patch.PatchesInfoImpl
me.yezhou.robust D/robust: current path:me.yezhou.robust.MainActivity
me.yezhou.robust D/robust: oldClass :class me.yezhou.robust.MainActivity     fields 3
me.yezhou.robust D/robust: current path:me.yezhou.robust.MainActivity find:ChangeQuickRedirect com.meituan.robust.patch.MainActivityPatchControl
me.yezhou.robust D/robust: changeQuickRedirectField set success com.meituan.robust.patch.MainActivityPatchControl
me.yezhou.robust D/robust: current path:me.yezhou.robust.SecondActivity
me.yezhou.robust D/robust: oldClass :class me.yezhou.robust.SecondActivity     fields 4
me.yezhou.robust D/robust: current path:me.yezhou.robust.SecondActivity find:ChangeQuickRedirect com.meituan.robust.patch.SecondActivityPatchControl
me.yezhou.robust D/robust: changeQuickRedirectField set success com.meituan.robust.patch.SecondActivityPatchControl
me.yezhou.robust D/robust: patch finished 
me.yezhou.robust D/RobustCallBack: onPatchApplied result: true
me.yezhou.robust D/RobustCallBack: onPatchApplied patch: appblog.cn
me.yezhou.robust D/robust: patch LocalPath:/storage/emulated/0/robust/patch.jar,apply result true

样例使用

(1)生成样例apk,执行gradle命令:

./gradlew clean assembleRelease --stacktrace --no-daemon

(2)安装样例apk。保存mapping.txt文件以及app/build/outputs/robust/methodsMap.robust文件

(3)修改代码之后,加上@Modify注解或者调用RobustModify.modify()方法

(4)把保存的mapping.txtmethodsMap.robust放到app/robust目录下

(5)执行与生成样式apk相同的gradle命令:

./gradlew clean  assembleRelease --stacktrace --no-daemon

(6)补丁制作成功后会停止构建apk,出现类似于如下的提示,表示补丁生成成功 补丁制作成功图片

Caused by: java.lang.RuntimeException: auto patch end successfully
        at robust.gradle.plugin.AutoPatchTransform.transform(AutoPatchTransform.groovy:103)
        at com.android.build.api.transform.Transform.transform(Transform.java:314)
        at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:284)
        at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:247)
        at com.android.builder.profile.ThreadRecorder.record(ThreadRecorder.java:106)

将补丁文件copy到手机目录/sdcard/robust

adb push ~/Desktop/code/robust/app/build/outputs/robust/patch.jar /sdcard/robust/patch.jar

(7)补丁的路径/sdcard/robustPatchManipulateImp中指定的

(8)打开App,点击Patch按钮就会加载补丁

(9)也可以加载app/robust的样例补丁,修改了Jump_second_Activity跳转Activity的显示文字。在样例中已经给类SecondActivity的方法getTextInfo(String meituan)制作补丁,可以自行定制

注意事项

(1)内部类的构造方法是privateprivate会生成一个匿名的构造函数)时,需要在制作补丁过程中手动修改构造方法的访问域为public

(2)对于方法的返回值是this的情况现在支持不好,比如builder模式,但在制作补丁代码时,可以通过如下方式来解决,增加一个类来包装一下(如下面的B类),

method a() {
  return this;
}

改为

method a() {
  return new B().setThis(this).getThis();
}

(3)字段增加能力内测中,不过暂时可以通过增加新类,把字段放到新类中的方式来实现字段增加能力

(4)新增的类支持包括静态内部类和非内部类

(5)对于只有字段访问的函数无法直接修复,可通过调用处间接修复

(6)构造方法的修复内测中

(7)资源和so的修复内测中

采坑记录

坑一:robust.xml配置问题

若patch报如下错误:

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:transformClassesWithAutoPatchTransformForDebug'.
    ...
Caused by: org.codehaus.groovy.GroovyException: patch method me.yezhou.robust.MainActivity.clickMe(android.view.View) haven't insert code by Robust.Cannot patch this method, method.signature  (Landroid/view/View;)V

原因robust.xmlhotfixPackage未配置正确,导致未Robust未植入代码

<!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码-->
<!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名,
这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的-->
<packname name="hotfixPackage">
    <name>me.yezhou</name>
    <name>cn.appblog</name>
</packname>

坑二:重启失效

Patch能够实时生效,但是杀死App进程,重新启动APP后补丁就会失效。因为杀死进程之后,内存中所有的信息都被清除,所以需要应用每次启动都去加载本地缓存的补丁

也就是说补丁加载是需要每次启动都主动加载的

为了安全,Robust使用完解密的补丁就会自动删除,补丁管理的逻辑需要自己控制

坑三:关于新增方法无效问题

参考:

开启混淆即可,未尝试

附:代码

原代码

package me.yezhou.robust;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.meituan.robust.PatchExecutor;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "yezhou";
    private static final int REQUEST_CODE_SDCARD_READ = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //RobustModify.modify();
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i(TAG, "MainActivity.onCreate");
    }

    private void runRobust() {
        new PatchExecutor(getApplicationContext(), new PatchManipulateImpl(), new RobustCallBackSample()).start();
    }

    //@Modify
    public void clickMe(View view) {
        Log.i(TAG, "MainActivity.clickMe");
        Toast.makeText(this, "AppBlog.CN", Toast.LENGTH_SHORT).show();
    }

    //@Add
    public String getString() {
        return "http://www.appblog.cn";
    }

    public void patch(View view) {
        if (isGrantSDCardReadPermission()) {
            runRobust();
        } else {
            requestPermission();
        }
    }

    private boolean isGrantSDCardReadPermission() {
        return PermissionUtils.isGrantSDCardReadPermission(this);
    }

    private void requestPermission() {
        PermissionUtils.requestSDCardReadPermission(this, REQUEST_CODE_SDCARD_READ);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_SDCARD_READ:
                handlePermissionResult();
                break;
            default:
                break;
        }
    }

    private void handlePermissionResult() {
        if (isGrantSDCardReadPermission()) {
            runRobust();
        } else {
            Toast.makeText(this, "failure because without sd card read permission", Toast.LENGTH_SHORT).show();
        }
    }

    public void jump(View view) {
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        startActivity(intent);
    }
}
package me.yezhou.robust;

import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import java.lang.reflect.Field;

public class SecondActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "yezhou";
    private ListView listView;
    private String[] multiArr = {"列表1", "列表2", "列表3", "列表4"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        listView = (ListView) findViewById(R.id.listview);
        TextView textView = (TextView) findViewById(R.id.secondtext);
        textView.setOnClickListener(v -> {
                //RobustModify.modify();
                Log.d(TAG, "OnClick in Listener");
            }
        );
        //change text on the  SecondActivity
        textView.setText(getTextInfo());

        //test array
        BaseAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, multiArr);
        listView.setAdapter(adapter);
        printLog(TAG, new String[][]{new String[]{"1", "2", "3"}, new String[]{"4", "5", "6"}});
    }

    //@Modify
    private String getTextInfo() {
//        String[] array = getArray();
//        Log.i(TAG, array[0] + " " + array[1]);
        Log.i(TAG, "SecondActivity.getTextInfo");
        return "error occur";
        //return "error fixed";
    }

//    @Add
//    public String[] getArray() {
//        return new String[]{"hello", "world"};
//    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return super.onCreateView(name, context, attrs);
    }

    @Override
    public void onClick(View v) {
        Toast.makeText(SecondActivity.this, "from implements onclick ", Toast.LENGTH_SHORT).show();
    }

    public static Field getReflectField(String name, Object instance) throws NoSuchFieldException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Field field = clazz.getDeclaredField(name);
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                return field;
            } catch (NoSuchFieldException e) {
                // ignore and search next
            }
        }
        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

    public static Object getFieldValue(String name, Object instance) {
        try {
            return getReflectField(name, instance).get(instance);
        } catch (Exception e) {
            Log.d(TAG, "getField error " + name + "   target   " + instance);
            e.printStackTrace();
        }
        return null;
    }

    private void printLog(@NonNull String tag, @NonNull String[][] args) {
        int i = 0;
        int j = 0;
        for (String[] array : args) {
            for (String arg : array) {
                Log.d(tag, "args[" + i + "][" + j + "] is: " + arg);
                j++;
            }
            i++;
        }
    }
}

修复代码

package me.yezhou.robust;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.meituan.robust.PatchExecutor;
import com.meituan.robust.patch.RobustModify;
import com.meituan.robust.patch.annotaion.Add;
import com.meituan.robust.patch.annotaion.Modify;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "yezhou";
    private static final int REQUEST_CODE_SDCARD_READ = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        RobustModify.modify();
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i(TAG, "MainActivity.Robust.onCreate");
    }

    private void runRobust() {
        new PatchExecutor(getApplicationContext(), new PatchManipulateImpl(), new RobustCallBackSample()).start();
    }

    @Modify
    public void clickMe(View view) {
        Log.i(TAG, "MainActivity.Robust.clickMe");
        //Toast.makeText(this, "AppBlog.CN", Toast.LENGTH_SHORT).show();
        Toast.makeText(this, getString(), Toast.LENGTH_SHORT).show();
    }

    @Add
    public String getString() {
        Log.i(TAG, "MainActivity.Robust.getString");
        return "http://www.appblog.cn";
    }

    public void patch(View view) {
        if (isGrantSDCardReadPermission()) {
            runRobust();
        } else {
            requestPermission();
        }
    }

    private boolean isGrantSDCardReadPermission() {
        return PermissionUtils.isGrantSDCardReadPermission(this);
    }

    private void requestPermission() {
        PermissionUtils.requestSDCardReadPermission(this, REQUEST_CODE_SDCARD_READ);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_SDCARD_READ:
                handlePermissionResult();
                break;
            default:
                break;
        }
    }

    private void handlePermissionResult() {
        if (isGrantSDCardReadPermission()) {
            runRobust();
        } else {
            Toast.makeText(this, "failure because without sd card read permission", Toast.LENGTH_SHORT).show();
        }
    }

    public void jump(View view) {
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        startActivity(intent);
    }
}
package me.yezhou.robust;

import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.meituan.robust.patch.RobustModify;
import com.meituan.robust.patch.annotaion.Add;
import com.meituan.robust.patch.annotaion.Modify;

import java.lang.reflect.Field;

public class SecondActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "yezhou";
    private ListView listView;
    private String[] multiArr = {"列表1", "列表2", "列表3", "列表4"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        listView = (ListView) findViewById(R.id.listview);
        TextView textView = (TextView) findViewById(R.id.secondtext);
        textView.setOnClickListener(v -> {
                RobustModify.modify();
                Log.d(TAG, "Robust OnClick in Listener");
            }
        );
        //change text on the  SecondActivity
        textView.setText(getTextInfo());

        //test array
        BaseAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, multiArr);
        listView.setAdapter(adapter);
        printLog(TAG, new String[][]{new String[]{"1", "2", "3"}, new String[]{"4", "5", "6"}});
    }

    @Modify
    private String getTextInfo() {
        Log.i(TAG, "SecondActivity.Robust.getTextInfo");
        String[] array = getArray();
        Log.i(TAG, array[0] + " " + array[1]);
        //return "error occur";
        return "error fixed";
    }

    @Add
    public String[] getArray() {
        Log.i(TAG, "SecondActivity.Robust.getArray");
        return new String[]{"hello", "world"};
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return super.onCreateView(name, context, attrs);
    }

    @Override
    public void onClick(View v) {
        Toast.makeText(SecondActivity.this, "from implements onclick ", Toast.LENGTH_SHORT).show();
    }

    public static Field getReflectField(String name, Object instance) throws NoSuchFieldException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Field field = clazz.getDeclaredField(name);
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                return field;
            } catch (NoSuchFieldException e) {
                // ignore and search next
            }
        }
        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

    public static Object getFieldValue(String name, Object instance) {
        try {
            return getReflectField(name, instance).get(instance);
        } catch (Exception e) {
            Log.d(TAG, "getField error " + name + "   target   " + instance);
            e.printStackTrace();
        }
        return null;
    }

    private void printLog(@NonNull String tag, @NonNull String[][] args) {
        int i = 0;
        int j = 0;
        for (String[] array : args) {
            for (String arg : array) {
                Log.d(tag, "args[" + i + "][" + j + "] is: " + arg);
                j++;
            }
            i++;
        }
    }
}

辅助类

package me.yezhou.robust;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import com.meituan.robust.Patch;
import com.meituan.robust.PatchManipulate;
import com.meituan.robust.RobustApkHashUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 推荐继承PatchManipulate实现App独特的A补丁加载策略,其中setLocalPath设置补丁的原始路径,这个路径存储的补丁是加密过得,setTempPath存储解密之后的补丁,是可以执行的jar文件
 * setTempPath设置的补丁加载完毕即刻删除,如果不需要加密和解密补丁,两者没有啥区别
 * author:yezhou
 * date:2020/8/28
 */
public class PatchManipulateImpl extends PatchManipulate {
    private static final String TAG = "yezhou";

    /***
     * connect to the network, get the latest patches
     * l联网获取最新的补丁
     * @param context
     * @return
     */
    @Override
    protected List<Patch> fetchPatchList(Context context) {
        //将app自己的robustApkHash上报给服务端,服务端根据robustApkHash来区分每一次apk build来给app下发补丁
        //apkhash is the unique identifier for  apk,so you cannnot patch wrong apk.
        String robustApkHash = RobustApkHashUtils.readRobustApkHash(context);
        Log.w("robust", "robustApkHash :" + robustApkHash);
        //connect to network to get patch list on servers
        //在这里去联网获取补丁列表
        Patch patch = new Patch();
        patch.setName("appblog.cn");
        //we recommend LocalPath store the origin patch.jar which may be encrypted,while TempPath is the true runnable jar
        //LocalPath是存储原始的补丁文件,这个文件应该是加密过的,TempPath是加密之后的,TempPath下的补丁加载完毕就删除,保证安全性
        //这里面需要设置一些补丁的信息,主要是联网的获取的补丁信息。重要的如MD5,进行原始补丁文件的简单校验,以及补丁存储的位置,这边推荐把补丁的储存位置放置到应用的私有目录下,保证安全性
        patch.setLocalPath(Environment.getExternalStorageDirectory().getPath() + File.separator + "robust" + File.separator + "patch");

        //setPatchesInfoImplClassFullName 设置项各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是和xml配置项patchPackname保持一致,而且类名必须是:PatchesInfoImpl
        //请注意这里的设置
        patch.setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl");
        List patches = new ArrayList<Patch>();
        patches.add(patch);
        return patches;
    }

    /**
     * you can verify your patches here
     *
     * @param context
     * @param patch
     * @return
     */
    @Override
    protected boolean verifyPatch(Context context, Patch patch) {
        //do your verification, put the real patch to patch
        //放到app的私有目录
        patch.setTempPath(context.getCacheDir() + File.separator + "robust" + File.separator + "patch");
        //in the sample we just copy the file
        try {
            copy(patch.getLocalPath(), patch.getTempPath());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("copy source patch to local patch error, no patch execute in path " + patch.getTempPath());
        }

        return true;
    }

    public void copy(String srcPath, String dstPath) throws IOException {
        File src = new File(srcPath);
        if (!src.exists()) {
            throw new RuntimeException("source patch does not exist ");
        }
        File dst = new File(dstPath);
        if (!dst.getParentFile().exists()) {
            dst.getParentFile().mkdirs();
        }
        InputStream in = new FileInputStream(src);
        try {
            OutputStream out = new FileOutputStream(dst);
            try {
                // Transfer bytes from in to out
                byte[] buf = new byte[1024];
                int len;
                while ((len = in.read(buf)) > 0) {
                    out.write(buf, 0, len);
                }
            } finally {
                out.close();
            }
        } finally {
            in.close();
        }
    }

    /**
     * @param patch
     * @return you may download your patches here, you can check whether patch is in the phone
     */
    @Override
    protected boolean ensurePatchExist(Patch patch) {
        return true;
    }
}
package me.yezhou.robust;

import android.util.Log;

import com.meituan.robust.Patch;
import com.meituan.robust.RobustCallBack;

import java.util.List;

public class RobustCallBackSample implements RobustCallBack {

    private static final String TAG = RobustCallBack.class.getSimpleName();

    @Override
    public void onPatchListFetched(boolean result, boolean isNet, List<Patch> patches) {
        Log.d(TAG, "onPatchListFetched result: " + result);
        Log.d(TAG, "onPatchListFetched isNet: " + isNet);
        for (Patch patch : patches) {
            Log.d(TAG, "onPatchListFetched patch: " + patch.getName());
        }
    }

    @Override
    public void onPatchFetched(boolean result, boolean isNet, Patch patch) {
        Log.d(TAG, "onPatchFetched result: " + result);
        Log.d(TAG, "onPatchFetched isNet: " + isNet);
        Log.d(TAG, "onPatchFetched patch: " + patch.getName());
    }

    @Override
    public void onPatchApplied(boolean result, Patch patch) {
        Log.d(TAG, "onPatchApplied result: " + result);
        Log.d(TAG, "onPatchApplied patch: " + patch.getName());
    }

    @Override
    public void logNotify(String log, String where) {
        Log.d(TAG, "logNotify log: " + log);
        Log.d(TAG, "logNotify where: " + where);
    }

    @Override
    public void exceptionNotify(Throwable throwable, String where) {
        Log.e(TAG, "exceptionNotify where: " + where, throwable);
    }
}
package me.yezhou.robust;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;

import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

public class PermissionUtils {
    /**
     * 是否有权限
     *
     * @param context
     * @return
     */
    public static boolean checkSelfPermission(Context context, String permission) {
        if (null == context) {
            return false;
        }
        int per = ContextCompat.checkSelfPermission(context, permission);
        return per == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Check that all given permissions have been granted by verifying that each entry in the
     * given array is of the value {@link PackageManager#PERMISSION_GRANTED}.
     *
     * @see Activity#onRequestPermissionsResult(int, String[], int[])
     */
    public static boolean verifyPermissions(int[] grantResults) {
        // At least one result must be checked.
        if (null == grantResults || grantResults.length < 1) {
            return false;
        }

        // Verify that each required permission has been granted, otherwise return false.
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    public static boolean isGrantSDCardReadPermission(Context context) {
        return checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE);
    }

    public static void requestSDCardReadPermission(Activity activity, int requestCode) {
        ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, requestCode);
    }
}

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/30/meituan-robust-hot-repair-access-practice/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
美团Robust热修复接入实践
Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明 编译打包阶段自动为每个class都增加了一个类型为Ch……
<<上一篇
下一篇>>
文章目录
关闭
目 录