美文网首页Android技术知识Android开发Android进阶之路
Android热修复方案第一弹——Tinker篇

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

作者: Mersens | 来源:发表于2017-04-06 16:34 被阅读0次

    背景

    一款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算法解析

    相关文章

      网友评论

        本文标题:Android热修复方案第一弹——Tinker篇

        本文链接:https://www.haomeiwen.com/subject/sfotattx.html