安卓面经_安卓进阶_安卓中高级工程师(5/15)之热修复

牛客高级系列专栏:

安卓

嵌入式

本人是2020年毕业于广东工业大学研究生:许乔丹,有国内大厂CVTE和世界500强企业安卓开发经验,该专栏整理本人对安卓进阶必备知识点的理解;

网上安卓资料千千万,笔者将继续维护专栏,一杯奶茶价格不止提供答案解析,更有专栏内容免费技术答疑,15大安卓进阶必备知识点包您懂,助您提高安卓进阶技术能力,为您高薪面试保驾护航!

正文开始⬇

学习安卓的热修复,一般是先了解热修复的概念,接着了解热修复的原理,最后了解几种常见的热修复框架。本文就是按照这个思路进行讲解。

1、热修复的概念

安卓的热修复(HotFix)是一种动态修复应用程序中的bug或漏洞的技术。它允许开发者在不重新发布整个应用程序的情况下,通过修复补丁来修复应用中的问题,减少用户的等待时间,提供了更好的用户体验,也提高了开发效率。

正常开发流程

alt

热修复开发流程

alt

国内热修复的框架有很多,但其核心功能主要可以归纳为三种:

  • 资源修复
  • 代码修复
  • 动态链接库修复

2、资源修复

2.1 Instant Run的概念

很多热修复框架的资源修复功能的实现,都是参考了Instant Run的资源修复原理。当你使用Android Studio进行开发时,每当你对应用程序的代码进行更改时,通常需要重新构建整个应用程序并重新安装在设备上进行调试。这个过程可能会耗费大量的时间,尤其是在大型项目中。

Instant Run是Android Studio2.0 提供的一项功能,旨在加快应用程序的开发和调试过程。翻译成中文叫即时运行或直接运行模板。就是下图的闪电图标。

alt

2.2 Instant Run的工作步骤

Instant Run使用了一种增量编译和部署的技术,它允许开发者在进行代码更改后,无需重新构建整个应用程序,就可以快速地将更改应用到设备上。Instant Run的工作步骤是:

  1. 初始安装:当你第一次运行应用程序时,Android Studio会将应用程序安装到设备上,并记录构建的一些元数据。这些元数据包括应用程序的版本号、签名信息、资源文件的哈希值等。
  2. 增量编译:当你对应用程序的代码进行更改时,Instant Run只会编译和构建发生更改的部分。这意味着它只会重新编译受到更改影响的类或方法,而不是整个应用程序。
  3. Instant Run会根据代码的情况决定采用哪种部署方式,当然,无论使用哪种方式你都不需要重新安装APP:
  • 热交换:一旦增量编译完成,Instant Run会将更改的字节码注入到正在运行的应用程序中,这个过程称为热交换(Hot Swap)。在热交换期间,Instant Run会尽可能保持应用程序的运行状态和数据,以确保无需重新启动应用程序即可查看代码更改的效果。比如修改一个现有方法中的代码时会采用热交换。
  • 温交换:App不需要重启,但Activity需要重启,当修改或者删除一个现有的资源文件时会采用温交换(Warm Swap)。
  • 冷交换:有时候,某些更改可能无法通过热交换应用到运行中的应用程序中。在这种情况下,Instant Run会执行冷交换(Cold Swap)。冷交换会重新启动应用程序,但只会重新加载受到更改影响的类,而不会重新初始化整个应用程序。比如添加、删除或修改一个变量或者方法,添加一个类等情况就会采用冷交换。

需要注意的是,Instant Run并不支持所有的代码更改。例如,如果你更改了应用程序的资源文件、Manifest文件、构建配置文件等,Instant Run可能无法自动应用这些更改,而需要重新构建整个应用程序。

下面以一个简单的示例来说明Instant Run的使用方法:

  1. 在Android Studio中创建一个新的Android项目,并打开MainActivity.java文件。

  2. 将MainActivity中的代码更改为以下内容:

    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            TextView textView = findViewById(R.id.textView);
            textView.setText("Hello Instant Run!");
        }
    }
    
  3. 运行应用程序,可以看到屏幕上显示了"Hello Instant Run!"。

  4. 现在,将TextView的文本更改为"Hello Instant Run Updated!",并保存更改。

  5. 点击闪电图标。

  6. 应用程序会在设备上重新启动,并且屏幕上显示了"Hello Instant Run Updated!"。

通过上述步骤,可以看到Instant Run的效果:在进行代码更改后,只需要点击一次按钮,就可以快速地将更改应用到设备上,而无需重新构建整个应用程序。

PS:在Android Studio 3.5版本后,废弃Instant Run功能,使用Apply Changes来代替。

alt

2.3 Instant Run的资源修复原理

刚说过,很多热修复框架的资源修复的原理都是参考Instant Run的资源修复原理,所以我们了解Instant Run的资源修复原理就可以了。Instant Run并非Android源码,需要通过反编译才可以获取到,其核心原理在MonkeyPatcher的monkeyPatchExistingResources()方法中,介绍原理前,我们需要要先了解AssetManager和mAssets。

  • AssetManager:AssetManager是Android中的一个类,用于访问应用程序的资源文件夹中的资源。通过AssetManager可以从assets文件夹中读取和访问应用程序所需的各种资源,例如图片、音频、视频等。
  • mAssets:mAssets是一个私有成员变量,通常是在Activity或Fragment中定义的。它是一个AssetManager对象的实例,用于在当前上下文中访问应用程序的资源。

我们看看如何使用AssetManager和mAssets来访问assets文件夹中的资源:

import android.content.res.AssetManager;
import android.content.Context;

public class MyActivity extends Activity {
    private AssetManager mAssets;

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

        // 获取AssetManager实例
        mAssets = getAssets();

        try {
            // 使用AssetManager打开assets文件夹中的文件
            InputStream inputStream = mAssets.open("my_file.txt");

            // 读取文件内容
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = reader.readLine()) != null) {
                // 处理文件内容
                Log.d("MyActivity", line);
            }

            // 关闭输入流
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,首先通过getAssets()方法获取了当前上下文的AssetManager实例,并将其赋值给mAssets变量。然后,使用mAssets的open()方法打开了assets文件夹中的一个文件(例如"my_file.txt")。接下来,通过InputStream读取文件内容,并进行相应的处理。最后,记得在不需要使用InputStream时关闭它。

回到Instant Run的monkeyPatchExistingResources()方法原理,可以简单总结为:

  • 首先创建新的AssetManger实例,命名为 newAssetManager,并通过反射调用类中的addAssetPath()方法,该方法可以加载外部资源,比如SD卡里的资源。这样newAssetManager就拥有了新的外部资源。
  • 接着通过反射得到Resources和Resources.Theme的mAssets字段,并调用mAssets.set()方法,将mAssets字段的引用替换为新创建的newAssetManager。
  • 最后,根据SDK版本不同,用不同方式得到Resources的弱引用集合,遍历集合里面的每个Resouces,同样得到对应的mAssets字段,再调用mAssets.set()方法,将mAssets字段的引用替换为新创建的newAssetManager。

经过这三个步骤后,就完成了资源修复。

3、代码修复

代码修复的方案一般可以分为以下3种:

  • 底层替换方案;
  • 类加载方案;
  • Instant Run方案;

3.1 类加载方案

3.1.1 65536限制和LinearAlloc限制

类加载方案基于Dex分包方案,说道Dex分包方案的由来,就需要从安卓65536限制和LinearAlloc限制说起:

安卓65536限制

安卓65536限制是指在单个应用程序的Dex文件中,方法的数量不能超过65536个的限制。这个限制是由于在Dalvik虚拟机中,使用16位来表示方法的索引,所以最大索引值为65535。其中一个索引留给了保留索引0,用于表示无效引用。因此,实际可用的方法索引数量为65536-1=65535。当应用程序的方法数量超过65536个时,可能会出现编译错误,导致应用程序无法构建或运行。这个限制主要会影响方法数较多的大型应用程序或包含多个库模块的应用程序。

为了解决这个限制,可以采取一些措施,如使用ProGuard进行代码混淆和优化,以减少方法数量;使用MultiDex技术将应用程序拆分成多个Dex文件,从而绕过限制;或者使用Android Gradle插件的分包工具来自动处理多Dex文件的配置。更多详情可以参考另一篇文章:《安卓APP完整开发流程(7/12)多Dex支持》

LinearAlloc限制

安卓的LinearAlloc限制是指在Dalvik虚拟机中,每个应用程序的LinearAlloc区域的大小有限制。LinearAlloc是Dalvik虚拟机中用于分配和加载方法及其相关数据的一块内存区域。LinearAlloc的大小限制是由于Dalvik虚拟机设计的一些限制和考虑所致。这个限制的具体数值在不同的Android版本中可能会有所不同,但通常在几十MB到几百MB之间。当应用程序的LinearAlloc区域超过限制时,可能会导致应用程序无法正常运行,或者出现内存溢出的情况。这个限制主要会影响方法数较多或者包含大量字符串常量的应用程序。

3.1.2 Dex分包方案

Dex文件是Android中用于存储应用程序的代码和资源的文件格式。Dex分包方案是为了解决65536方法限制和LinearAlloc限制而引入的。Dex分包将应用程序的代码和资源分为多个Dex文件,以克服Dex文件中方法数量的限制。最常见的Dex分包方案是MultiDex方案,这是安卓官方提供的解决方案。它允许应用程序在运行时加载多个Dex文件。使用MultiDex,可以在应用程序中包含多个Dex文件,并通过配置build.gradle文件来启用MultiDex支持。

当应用程序使用Dex分包方案时,类加载方案也会相应地发生变化。传统上,安卓应用程序使用单一的Dex文件进行类加载。但是,当使用Dex分包方案时,需要修改类加载器来加载多个Dex文件中的类。在使用MultiDex方案时,会使用MultiDex类加载器来加载主Dex文件(包含应用程序的核心类)和额外的分包Dex文件。这样,应用程序可以在运行时动态地加载和访问分包Dex文件中的类和方法。

3.1.3 类加载方案的原理

类加载方案是指在应用程序运行时,将类文件加载到内存中并创建相应的类对象的过程。在Java中,类加载器是负责执行这个任务的组件。类加载器负责从文件系统、网络等来源加载类文件,并将其转换为可执行的类对象。在了解类加载方案之前,需要对几个概念理清楚:

  • 类加载器的双亲委托模式:Java中的类加载器是一个层次结构,它由多个类加载器组成,每个类加载器负责加载特定的类文件。当一个类加载器需要加载一个类时,它会首先委托给其父类加载器来尝试加载。父类加载器会递归地将加载请求传递给其父类加载器,直到达到根类加载器。只有当所有的父类加载器都无法加载该类时,才由当前类加载器来加载。如此就可以防止重复加载,因为每个类加载器都会首先检查父类加载器是否已经加载了该类。从而确保同一个类在整个类加载器层次结构中只被加载一次,避免了类的冲突和重复加载的问题。
  • Element:代表了Java源代码中的一个元素,例如类、接口、字段、方法等。Element提供了访问和操作这些元素的方法,使开发人员能够在编译时获取有关源代码结构的信息。
  • dexElements:多个Element组成的有序Element数组。
  • DexFile:是Android平台中的一个类,Element内部封装了DexFile,代表了一个已经编译过的dex文件,即Android应用程序的字节码文件。DexFile类提供了一些方法,允许开发人员在运行时加载和访问dex文件中的类和方法。
  • dex文件:在Android应用程序安装时,dex文件会被加载到Dalvik虚拟机中,供应用程序在运行时使用,可以理解为一个安卓APK就是由多个dex文件组成的。可以用DexFile来加载dex文件。

Element、DexFile和dex文件之间的关系是:Element内部封装了DexFile,DexFile可以加载dex文件,因此,每个dex文件对应一个Element。换句话说,DexFile是dex文件的运行时表示形式,而Element则是源代码级别的抽象。

alt

上图是类加载方案的说明图,当我们要加载一个类,比如Student.class,看看如下流程:

正常流程

首先会遍历dexElements,也就是遍历每一个Element,上面说了,每个Element对应一个dex文件。此时会调用Element的fingClass()方法,该方法内部会调用DexFile类的loadClassBinaryName()方法查找类,如果在当前的Element找到类就返回(即在对应的dex文件找到了目标类),如果没有找到则继续遍历。

代码修复流程

为了修复代码,我们可以把出现Bug的类Student.class进行修改,并将Student.class打包成包含dex文件的补丁包Patch.dex。接着将其放在dexElements数组中的第一个元素。此时遍历dexElements数组时,会找到Patch.dex文件中的新的Student.class进行加载,根据类加载器的双亲委托模式,原来的Student.class将不再加载,从而替换原来存在Bug的旧的Student.class,这就是类加载方案实现代码修复的原理。

优点:

  • 实现简单
  • 不需要太多的适配

缺点:

  • 因为已加载的类是无法卸载的,因此使用类加载方案进行代码修复,是需要重启App来重新加载类,因此此方案的热修复框架是不能即时生效的。
  • Art 虚拟机上由于 oat 导致的地址偏移问题,可能会需要在补丁包中打入补丁无关的类,导致补丁包体积增大。

3.2 底层替换方案

上面说到类加载方案会重启App,而底层替换方案则不会再次加载类,而是直接在底层,即Native层修改原有类。在安卓或者Java代码中,最小组织方式就是方法,一个APK文件包含多个Dex文件,一个Dex文件有65536个方法,每个方法在ART虚拟机中都有一个名为ArtMethod的结构体指针与之对应,ArtMethod结构如下:

class ArtMethod FINAL {

protected:
  GcRoot<mirror::Class> declaring_class_;

  std::atomic<std::uint32_t> access_flags_;

  // Offset to the CodeItem.
  uint32_t dex_code_item_offset_;

  // Index into method_ids of the dex file associated with this method.
  uint32_t dex_method_index_;

  uint16_t method_index_;

  uint16_t hotness_count_;

  struct PtrSizedFields {
    // Depending on the method type, the data is
    //   - native method: pointer to the JNI function registered to this method
    //                    or a function to resolve the JNI function,
    //   - conflict method: ImtConflictTable,
    //   - abstract/interface method: the single-implementation if any,
    //   - proxy method: the original interface method or constructor,
    //   - other methods: the profiling data.
    ArtMethod** dex_cache_resolverd_methods;  //1
    
    void* data_;

    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;  //2
  } ptr_sized_fields_;
...
}

ArtMethod结构体是安卓系统中用于存储Java方法信息的重要数据结构。它包含了Java方法的执行入口、访问权限、所属类和代码执行地址等关键信息。换句话说,每次调用方法的时候,都是通过 ArtMet

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Android进阶知识体系解析 文章被收录于专栏

#提供免费售后答疑!!花一杯奶茶的钱获得安卓知识体系答疑服务,稳赚不赔# 当你已经掌握了Android基础知识,你可能会想要进一步深入学习安卓进阶知识。在这个专栏中,我们将探讨一些高级安卓开发技术,无论你是初学者还是有经验的开发者,这个专栏都将为你提供有价值的知识和经验。让我们一起开始探索安卓进阶知识的奇妙世界吧!

全部评论

相关推荐

TP-LINK 前端工程师 年包大概20出头 本科
点赞 评论 收藏
转发
2 5 评论
分享
牛客网
牛客企业服务