Android 热修复方案第一弹——Tinker 篇

1,578 阅读8分钟
原文链接: www.jianshu.com

背景

一款App的正常开发流程应该是这样的:新版本上线-->用户安装-->发现Bug-->紧急修复-->重新发布新版本-->提示用户安装更新,从表面上看这样的开发流程顺理成章,但存在 很多弊端:
1.耗时,代价大,有时候可能是一个很小很细微的一个问题,但你还必须下架并 更新应用版本。
2.用户体验差,安装成本高,一个很小的bug就要导致用户重新下载整个应用安装包来进行覆盖安装,也额外增加了用户的流量开支。
那么问题来了,有没有办法来实现动态的修复,不需要重新下载App,在用户无感知的情况下以较低的成本来修复Bug问题?答案是肯定的,热修复技术做得到。

概述

当前关于热修复的实现方案有很多,比较出名的有阿里的AndFix,美团的Robust,QZone的超级补丁以及微信的Tinker,这篇文章将对Tinker接入使用以及实现原理进行简单的分析,关于Tinker这里就不再赘述,对它不了解的可以点击这里 Tinker,值得注意的是Tinker并不是万能的,也有局限性:
1、Tinker不支持修改AndroidManifest.xml;
2、Tinker不支持新增四大组件;
3、在Android N上,补丁对应用启动时间有轻微的影响;
4、不支持部分三星android-21机型,加载补丁时会主动抛异常;
5、在1.7.6以及之后的版本,tinker不再支持加固的动态更新;
6、对于资源替换,不支持修改remoteView。例如transition动画,notification
icon以及桌面图标。
7、任何热修复技术都无法做到100%的成功修复。

接入

Tinker提供了两种接入方式:Gradle和命令行,在这里以Gradle依赖接入为例。
在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')
    }
}

在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.

dependencies {
    //可选,用于生成application类 
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker的核心库
    compile('com.tencent.tinker:tinker-android-lib:1.7.7') 
}
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'

签名配置

    signingConfigs {
        release {
            try {
                storeFile file("./keystore/release.keystore")
                storePassword "testres"
                keyAlias "testres"
                keyPassword "testres"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }

        debug {
            storeFile file("./keystore/debug.keystore")
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }

文件目录配置

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true
    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-debug-0406-10-59-13.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-debug-0406-10-59-13-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0406-10-59-13-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0406-10-59-13"
}

具体的参数设置事例可参考tinker sample中的app/build.gradle
新建一个Application在onCreate()方法中对Tinker进行初始化,不过Tinker自己提供了一套通过反射机制来实现Application,通过代码你会发现它并不是Application的子类,后面会详细介绍。

@SuppressWarnings("unused")
@DefaultLifeCycle(application = ".SampleApplication",
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);

    }

    @Override
    public void onCreate() {
        super.onCreate();
        TinkerInstaller.install(this);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

}

“application ”这个标签的name就是Application,必须与AndroidManifest.xml保持一致

    <application
        android:allowBackup="true"
        android:name=".SampleApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        ...
        ...
    </application>

在Activity中模拟热修复加载补丁来解决空指针异常,点击settext按钮为TextView设置“TINKER PATCH”,由于TextView没有进行初始化,因此会出现空指针异常。

public class MainActivity extends AppCompatActivity {
    private TextView tv_msg;
    private Button btn_loadpatch;
    private Button btn_settext;

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

    private void init() {
        //在此对TextView不进行初始化直接设置Text会出现空指针的异常       
        //tv_msg=(TextView)findViewById(R.id.tv_msg);
        btn_loadpatch=(Button)findViewById(R.id.btn_loadpatch);
        btn_settext=(Button)findViewById(R.id.btn_settext);       
        btn_settext.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //此处会报空指针异常
                tv_msg.setText("TINKER PATCH");
            }
        });
        //加载补丁
        btn_loadpatch.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                        Environment.getExternalStorageDirectory().getAbsolutePath() +
                                "/patch_unsigned.apk");
            }
        });
    }
}

通过Gradle编译后,就会在build/bakApk下生成本地打包的apk(Debug不会生成mapping文件)


bakApk


因为TextView没有进行初始化,接下来修改Activity代码,对TextView进行初始化,解决空指针异常。

public class MainActivity extends AppCompatActivity {
    private TextView tv_msg;
    private Button btn_loadpatch;
    private Button btn_settext;

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

    private void init() {
        //在此对TextView进行初始化,修复空指针异常
        tv_msg=(TextView)findViewById(R.id.tv_msg);
        btn_loadpatch=(Button)findViewById(R.id.btn_loadpatch);
        btn_settext=(Button)findViewById(R.id.btn_settext);

        btn_settext.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv_msg.setText("TINKER PATCH");
            }
        });
        btn_loadpatch.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                        Environment.getExternalStorageDirectory().getAbsolutePath() +
                                "/patch_unsigned.apk");
            }
        });
    }
}

可以通过gradlew命令来生成差分包,在此之前需要在app/build.gradle中设置相比较的两个app,其中app-debug-0406-10-33-27.apk就是需要类比的apk。

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true
    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-debug-0406-10-33-27.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-debug-0406-10-33-27-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0406-10-33-27-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0406-10-33-27"
}
./gradlew tinkerPatchRelease  //Release包

 ./gradlew tinkerPatchDebug  //Debug包

差分包存放在build/outputs/tinkerPatch目录下,patch_unsigned.apk为没有签名的补丁包,patch_signed.apk为已签名的补丁包,patch_signed_7zip.apk为签名后并使用7zip压缩的补丁包,也是Tinker推荐的一种使用方式,这里没有进行签名打包,所以选择使用patch_unsigned.apk差分包,并把该补丁包放在手机的sdcard中。


差分包


然后先点击“btn_loadpatch”按钮,去加载补丁,然后再点击“settext”按钮,可以看到空指针异常已经修复。
运行效果图:


运行效果图

运行原理

Tinker对两个App进行对比,找出差分包,即为patch.dex,然将patch.dex与应用的classes.dex合并整体替换掉旧的dex文件。

一、Application生成

Application的生成采用了java的注解方式,在编译时生成,在com.tencent.tinker.anno下面定义了一个注解方式。
从注解格式中可以看出:
1、描述的是一个类的实现
2、注解会被编译器丢弃,但它会保留源文件
3、该类是被继承的
4、定义体内的参数类型为:String,String,int boolean

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
public @interface DefaultLifeCycle {
    String application();

    String loaderClass() default "com.tencent.tinker.loader.TinkerLoader";

    int flags();

    boolean loadVerifyFlag() default false;
}

在com.tencent.tinker.anno包里面存放有一个TinkerApplication.tmpl的Application的模板:
%TINKER_FLAGS%对应flags
%APPLICATION_LIFE_CYCLE%,为ApplicationLike的全路径
%TINKER_LOADER_CLASS%,loaderClass属性
%TINKER_LOAD_VERIFY_FLAG%对应loadVerifyFlag

public class %APPLICATION% extends TinkerApplication {

    public %APPLICATION%() {
        super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
    }

}

自定义注解的实现,需要继承AbstractProcessor类,com.tencent.tinker.anno包下的AnnotationProcessor类继承该类并有具体的实现,在processDefaultLifeCycle方法中会循环遍历被DefaultLifeCycle标识的对象,获取注解中声明的数值,然后读取模板,填充数值,最终生成一个继承于TinkerApplication的Application实例

 private void processDefaultLifeCycle(Set<? extends Element> elements) {
        Iterator var2 = elements.iterator();

        while(var2.hasNext()) {
            Element e = (Element)var2.next();
            DefaultLifeCycle ca = (DefaultLifeCycle)e.getAnnotation(DefaultLifeCycle.class);
            String lifeCycleClassName = ((TypeElement)e).getQualifiedName().toString();
            String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf(46));
            lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf(46) + 1);
            String applicationClassName = ca.application();
            if(applicationClassName.startsWith(".")) {
                applicationClassName = lifeCyclePackageName + applicationClassName;
            }

            String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf(46));
            applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf(46) + 1);
            String loaderClassName = ca.loaderClass();
            if(loaderClassName.startsWith(".")) {
                loaderClassName = lifeCyclePackageName + loaderClassName;
            }

            System.out.println("*");
            InputStream is = AnnotationProcessor.class.getResourceAsStream("/TinkerAnnoApplication.tmpl");
            Scanner scanner = new Scanner(is);
            String template = scanner.useDelimiter("\\A").next();
            String fileContent = template.replaceAll("%PACKAGE%", applicationPackageName).replaceAll("%APPLICATION%", applicationClassName).replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName).replaceAll("%TINKER_FLAGS%", "" + ca.flags()).replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName).replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());

            try {
                JavaFileObject x = this.processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName, new Element[0]);
                this.processingEnv.getMessager().printMessage(Kind.NOTE, "Creating " + x.toUri());
                Writer writer = x.openWriter();

                try {
                    PrintWriter pw = new PrintWriter(writer);
                    pw.print(fileContent);
                    pw.flush();
                } finally {
                    writer.close();
                }
            } catch (IOException var21) {
                this.processingEnv.getMessager().printMessage(Kind.ERROR, var21.toString());
            }
        }

    }

二、执行流程

在TinkerApplication的onBaseContextAttached()方法调用loadTinker()方法

private void loadTinker() {
        //disable tinker, not need to install
        if (tinkerFlags == TINKER_DISABLE) {
            return;
        }
        tinkerResultIntent = new Intent();
        try {
            //reflect tinker loader, because loaderClass may be define by user!
            Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());

            Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class, int.class, boolean.class);
            Constructor<?> constructor = tinkerLoadClass.getConstructor();
            tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this, tinkerFlags, tinkerLoadVerifyFlag);
        } catch (Throwable e) {
            //has exception, put exception error code
            ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
            tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
        }
    }

在loadTinker中通过反射的方式调用TinkerLoader中的tryLoad方法

    @Override
    public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
        Intent resultIntent = new Intent();

        long begin = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

在tryLoadPatchFilesInternal()方法中加载本地补丁,进行dex文件对比判断并添加到dexList中

        if (isEnabledForDex) {
            //tinker/patch.info/patch-641e634c/dex
            boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
            if (!dexCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:dex check fail");
                return;
            }
        }
        //now we can load patch jar
        if (isEnabledForDex) {
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
            if (!loadTinkerJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
                return;
            }
        }

        //now we can load patch resource
        if (isEnabledForResource) {
            boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);
            if (!loadTinkerResources) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
                return;
            }
        }

然后在核心类SystemClassLoaderAdde中的installDexes进行修复,Android版本的不同,采用的方法也不同,在installDexes对Android的版本进行判断执行相应的操作,然后对Element[]数组进行组合,保存到pathList

private static final class V23 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            /* The patched class loader is expected to be a descendant of
             * dalvik.system.BaseDexClassLoader. We modify its
             * dalvik.system.DexPathList pathList field to append additional DEX
             * file entries.
             */
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makePathElement", e);
                    throw e;
                }

            }
        }

Tinker开启TinkerPatchService来执行合并操作,TinkerPatchService继承于IntentService,只用关注onHandleIntent()方法,在该方法调用UpgradePatch.tryPatch(),最终在DexDiffPatchInternal类中extractDexDiffInternals方法进行合并

 @Override
    protected void onHandleIntent(Intent intent) {
        final Context context = getApplicationContext();
        Tinker tinker = Tinker.with(context);
        tinker.getPatchReporter().onPatchServiceStart(intent);

        if (intent == null) {
            TinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");
            return;
        }
        String path = getPatchPathExtra(intent);
        if (path == null) {
            TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
            return;
        }
        File patchFile = new File(path);

        long begin = SystemClock.elapsedRealtime();
        boolean result;
        long cost;
        Throwable e = null;

        increasingPriority();
        PatchResult patchResult = new PatchResult();
        try {
            if (upgradePatchProcessor == null) {
                throw new TinkerRuntimeException("upgradePatchProcessor is null.");
            }
            result = upgradePatchProcessor.tryPatch(context, path, patchResult);
        } catch (Throwable throwable) {
            e = throwable;
            result = false;
            tinker.getPatchReporter().onPatchException(patchFile, e);
        }

        cost = SystemClock.elapsedRealtime() - begin;
        tinker.getPatchReporter().
            onPatchResult(patchFile, result, cost);

        patchResult.isSuccess = result;
        patchResult.rawPatchFilePath = path;
        patchResult.costTime = cost;
        patchResult.e = e;

        AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

    }

关于Tinker的 合并算法可以参考 Tinker Dexdiff算法解析