Robust 2.0:支持Android R8的升级版热修复框架

news2024/11/20 1:27:50

2016年,我们对美团Android热更新方案Robust的技术原理做了详细介绍。近几年,Google 推出了新的代码优化混淆工具R8,Android 热修复补丁制作依赖二次构建包和线上包对比,需要对Proguard切换到R8提前进行适配和改造,本文分享 Robust 在适配 R8 以及优化改进中的一些思路和经验,希望能对大家有所帮助或者启发。

  • 1. 背景

  • 2. 主要挑战

  • 3 解决思路

    • 3.1 整体方案介绍

    • 3.2 问题和解决方法

  • 4 总结

  • 5 本文作者

 1. 背景 

美团 Robust 是基于方法插桩的实时热修复框架,主要优势是实时生效、零 Hook 兼容所有 Android 版本。2016 年,我们在《Android 热更新方案 Robust》一文中对技术原理做了详细介绍,主要通过给每个方法插入 IF 分支来动态控制代码逻辑,进而实现热修复。其核心主要有两部分:一个是代码插桩,一个是自动补丁。

  • 代码插桩这部分随着 Javassist、ASM 工具的广泛使用,整体方案比较成熟了,迭代改进主要是针对插桩代码体积和性能的优化;

  • 自动补丁这部分在实际使用过程中一直在迭代,跟业界主流热修复方案一样,自动化补丁工具作制作时机是在 Proguard 混淆之后,由于 Proguard 会对代码进行代码优化和混淆处理,在 Proguard 后制作补丁能够降低补丁生成的复杂性。

近年来, Google 推出了新的代码优化混淆工具 R8,用于取代第三方的代码优化混淆工具 Proguard,经过多年功能迭代和缺陷改进,R8 在功能上基本可以替代 Proguard,在结果上更为出色(优化生成的 Android 字节码体积更小)。Google 已经在新版本的构建工具中强制使用 R8 ,国内外已有多个知名 App 完成了 R8 适配并上线,比如微信 Android 在今年正式从 Proguard 切换到了 R8(通过升级 Android 构建工具链)。Android 热修复补丁制作依赖二次构建包和线上包对比,需要对 Proguard 切换到 R8 提前进行适配和改造,本文分享了美团平台技术部 Robust 在适配 R8 以及优化改进中的一些思路和经验。

2. 主要挑战 

Android 热修复补丁的大致制作流程:首先基于线上代码进行逻辑修复并二次打包,然后补丁生成工具自动比较修复包和线上包的差异,最后制作出轻量的补丁包。因此在补丁制作的过程中,需要解决两个主要问题:

  • 对于没有变动的代码,如何在二次打包时保证和线上包一致;

  • 对于本次修复的代码,如何在经过编译、优化、混淆之后准确识别出来并生成补丁代码。

要解决这两个问题,需要对 Android 编译和构建过程有一定了解,弄清楚问题产生的原因。下图 1 是一个 Android 项目从源码到 APK(Android 应用安装包)的构建过程(椭圆形对应构建工具链):

29c0019a64df43c456a51e83ab46854f.png图1 从源码到 APK 的构建过程

上图有些工具已被新出现的工具所取代,但是整体的流程并没有太大变化。对照这个图,我们分析一下其中对补丁制作/二次打包有影响的几个环节:

  1. 资源编译器(aapt/aapt2):资源编译环节会生成一个 R.java 文件(记录着资源 id,便于代码中引用),一般为了解决 R field 过多以及减少包大小,大型 Android 项目会在构建过程中会将资源 id 直接内联到调用处(发生在 javac 和 proguard 之间)。如果前后两次打包出现资源 id 不一致,会影响 diff 识别的结果。

  2. 代码编译器(javac):Java 代码经过 javac 编译成字节码之后,除了有一些简单的优化(如常量表达式折叠、条件编译),还有一些基础的脱糖(Java 8 之前的语法特性)操作会生成一些新的类/方法/指令,如匿名内部类会被编译成一个名为 OuterClass$1.class 的新类,以及命名为 access$200 之类的桥方法。如果改动涉及内部类、泛型,二次打包$后面的数字编号可能和线上包出现乱序。

  3. 代码优化器(ProGuard/R8):目前主要使用第三方开源工具 ProGuard (Google 推出 R8 计划取代 Proguard),通过 30+ 可选优化项,对前面生成的 Java 字节码进一步压缩、优化、混淆,可以使得 Android 安装包更小、更加安全、运行效率更高:

  • 压缩:通过静态分析并删除未被使用的 class/field/method,即源码中存在的 class/field/method,线上包中不一定存在。

  • 优化:通过一系列优化算法或者模版,对字节码进行优化,使得构建产物更小、运行更高效/安全,优化手段有合并类/接口、内联短方法、裁剪方法参数、删除不可达分支、外联代码(R8 新增)、删除无副作用代码(如 Log.d())、修改方法/变量可见性等等。优化后的字节码相比源码,可能出现 class/field/method 数量减少、field/method 访问修饰符发生变化、method 签名发生变化、code 指令变少,另外二次构建优化结果可能和线上包不一致。

  • 混淆:通过将 class/field/method 的名称重命名为一个无意义的短字符,增加逆向难度,减少包大小。二次打包需要保证和线上包的混淆保持一致,不然补丁加载后因调用异常而发生崩溃。

脱糖工具(图中未标出,旧版本使用三方插件 Lambda/Desugar,新版本中使用自带的 R8):由于低版本 Android 设备不支持 Java 8+ 语法特性,这一步需要将 Lambda 表达式、方法引用、默认和静态接口方法等高版本的语法特性转为低版本实现。其中 Lambda 表达式会被编译成一个内部类,会有类似(2)中的问题。

至此,我们对本章开头提到的2个问题的产生原因有了一定认识,经过 Android 构建过程生成的字节码相比源码在 class/field/method/code 维度上有了“结构性”的变化,比如修复代码中调用的 class/field/method 在线上包中不存在(被 shrink、被 merge、被 inline),或者源码中可以访问、但在补丁中无法访问的 field/method(修饰符被标记为 private)、method parameter 列表匹配不上(之前没有被用到的 parameter 被裁剪了)等等。

Proguard 提供的这些优化项是可选的,一般情况下大型 Android 项目中会结合实际收益、稳定性以及构建耗时等多方因素综合考量后,会禁用一部分优化项,但并不是完全禁用。因此,二次打包时和线上包会产生一些差异,补丁制作准确性会受此影响。过去 Robust 补丁制作过程经常遇到此类问题,通过特殊字符检测、白名单等方式能够提升识别的准确性,但实现方案不够自动化。Robust 补丁制作流程如下:

bef4dfaaf82c37b41b2fe46565170769.png

图2 Robust 补丁制作流程

如果将 Android 项目的构建工具链(Android Gradle Plugin)升级到官方较新版本,上图中的 Proguard(Java字节码优化和) + Dex(Android 字节码生成) 两个环节将被合并成一个,并被替换成 R8:

0b4eac4981339d4aa9306b5508ebbd5c.png

图3 两种构建流

上述构建工具链的升级变化,给 Robust 补丁制作带来 2 个新的问题:

  1. 没有合适时机制作补丁。如果将基于 JAR 的改动识别方案,改成基于 DEX 或者 Smali,等同于更换补丁制作方案,前者需要基于 DEX 文件格式和指令,后者需要处理大量寄存器,更容易出错,兼容性和稳定性不够好。

  2. Proguard 可以禁用一部分优化选项,但是 R8 官方文档明确表示不支持禁用部分优化,相比之前会产生更多的差异,对改动识别造成干扰。

 3 解决思路 

| 3.1 整体方案介绍

基于 R8 构建的补丁制作思路是将改动识别提到优化混淆之前,对比 Java 字节码,同时结合对线上 APK 结构化解析(class/field/method),校正补丁代码对线上代码的调用,得到 patch.jar,最后借助 R8 对 patch.jar 进行混淆(applymapping)、脱糖、生成 Dex,打包得到 patch.apk,完整流程如下图所示:

9953512106e513eb43c328c60e0192ef.png

图4 完整流程

| 3.2 问题和解决方法

3.2.1 R8 与 Proguard 优化对比

部分 ProGuard 的配置项在切换到 R8 后失效,R8 官方文档对此做出的解释是:随着 R8 的不断改进,维护标准的优化行为有助于 Android Studio 团队轻松排查并解决您可能遇到的任何问题。

ce110f5cf45987bd9681c1f44a6b17e8.png

图5 R8 官方解释

截至目前,仍能在网上搜到不少因 R8 优化带来的问题,没有公开文档介绍优化规则的使用和禁用说明。只能通过阅读 ProGuard 官方文档和 R8 源码,对比分析两者优化规则的相似和差异。通过 R8 源码发现可以通过隐藏的构建参数、反射或者直接修改 R8 源码实现一部分规则禁用,虽然 R8 的优化规则并不是和 Proguard 一一对应,但也基本可以实现和之前使用 Proguard 时相同的优化效果。

com.android.tools.r8.utils.InternalOptions.enableEnumUnboxing
com.android.tools.r8.utils.InternalOptions.enableVerticalClassMerging
com.android.tools.r8.utils.InternalOptions.enableClassInlining
com.android.tools.r8.utils.InternalOptions.inlinerOptions().enableInlining//方法内联
com.android.tools.r8.utils.InternalOptions.outline.enabled)//方法外联
com.android.tools.r8.utils.InternalOptions.testing.disableMarkingMethodsFinal
com.android.tools.r8.utils.InternalOptions.testing.disableMarkingClassesFinal

一些规则可以通过构建参数-Dcom.android.tools.r8.disableMarkingMethodsFinal 来控制关闭/开启,其他不支持的参数也可以参考如下方式简单改造一下:

4123461f07d4559b80de9f8037eeed4d.png

图6 改造方式

如果某个项目中不希望禁用这些规则呢?在之前的补丁制作流程中,可能会影响改动识别的准确性。而在新的补丁制作流程中,改动识别不受影响,但在识别之后,还需要结合线上 APK 检查补丁中的外部调用是否合法。进一步仔细分析这些优化规则,可以分为 class、field、method、code 四类,其中对 Robust 补丁制作影响较大的是方法内联、参数移除、被标记为 private,后面的小节里将会介绍相应的处理方法。

3.2.2 “真”“假”改动识别

如果源码中有匿名内部类,javac 会编译生成一个命名为 {外部类名}${数字编号} 的类,后面的数字编号是根据该匿名内部类在外部类中出现的先后顺序,依次累加计算出来的。

088de303558baf20171da62a1c780f1c.png

当修复代码中有新增/删除匿名内部类时,仅通过类名无法比较(所以在一些以类为最小粒度的热修复框架使用文档里,会看到类似“不支持新增匿名内部类”、“只支持在外部类的末尾增加匿名内部类”之类的描述),这时候 Robust 会模糊处理后面的数字编号,通过字节码对比进一步查找到真实变化的匿名内部类,识别出哪些是真改,哪些是假改。

此外,如果嵌套类之间涉及私有 field/method 访问,javac 编译器会生成命名规则为 access$100、access$200 的桥接方法,access$后面的数字编号(和出现的先后顺序有关)也会影响改动识别(最终 R8 会将修饰符改成 public 并删除桥接方法),这里的解决办法和上面识别真实内部类改动的方式类似。

还有一种情况值得注意,大一点的 Android 项目通常会采用组件化的方式,每个组件以 AAR 形式参与 App 构建打包,在组件二进制发版(源码-> AAR)过程中,可以使用 R8 进行脱糖(For Android)得到 Java 7 字节码,典型的例子是 Lambda 表达式,经过脱糖处理生成  {外部类}$$ExternalSyntheticLambda{数字} (甚至有多重数字的情况如$2$1) 之类的 class,以及在外部类中生成命名规则为 lambda${方法名}${数字} 的静态方法(不同的脱糖器,命名规则不一样),补丁生成工具处理方法和上面类似。

最终识别出来的代码改动,不仅包含源码有改动的方法或者新增方法/类(如果有),还包括与之有关的、由 javac 编译器脱糖生成的字节码,以及由组件二进制发版过程中经 R8 脱糖生成的字节码。

3.2.3 内联识别与处理

通过第二章节的介绍,可以看到线上代码在经 javac 编译之后还会经过字节码优化、混淆等处理,因此,通过上面字节码比对识别出来的代码变更(class/method 维度),如果涉及对线上代码的调用,还需要确保这些 Field/Method 的调用是“合法”的,避免运行时崩溃。

在众多优化项当中,主要需要关注的是 class/field/method 是否存在、是否可访问。如果线上包中不存在(上次构建过程中被移除或者被内联),补丁生成阶段需要当做新增类/方法加进来;如果线上包中不可以被外部访问(上次构建过程中 public 被改为 private),补丁生成阶段需要将直接调用改成反射调用;如果线上包中方法签名发生变化(上次构建过程中参数被裁剪),需要修改调用或者当做新方法加进来。

由于 Dex 文件与标准的 class 文件在结构设计上有着本质的区别(Dex 工具将所有的 class 文件整合到一个或几个 Dex 文件,目的是其中各个类能够共享数据,使得文件结构更加经凑),两者无法直接对比。具体检测方法是先通过 ASM 分析补丁 class 中的外部引用,然后借助 dexlib2 库解析 APK 中的 Dex,提取出 class/field/method 结构化信息(还需反混淆处理),最后再兼容性分析和处理。

R8 外联优化是一种高级优化技术,生效条件非常苛刻,需要在合适的环境下合理使用,R8的外联优化会将多个方法中的相同代码提取到新方法中,以降低代码体积,但是会增加一次方法调用开销。如果恰好想修复的代码是被外联出去的方法,直接将外联方法当成新增方法来修复即可。

3.2.4 混淆问题与优化

不同于前面对在二次打包过程中对整个项目进行 ApplyMapping,这里只需要对少数发生变更的类进行 ApplyMapping,出现混淆不一致的概率会小很多。Robust 补丁制作过程中,仅将改动的类传递给 Proguard 进行二次混淆,这个过程中会自动应用线上包的 mapping 文件:

-applymapping {线上包的 mapping.txt}

但在某些特殊情形下,比如删了一个旧方法、同时又增加了一个新方法,或者是 ApplyMapping 的缺陷,还是会出现补丁中的混淆和线上混淆实际并不一致的情况,因此在生成补丁之后,还需要根据线上 APK 进行对比校验,如果发现错误混淆,进一步反编译成 Smali 之后进行字符替换。

3.2.5 其他方面的优化

(1)super 指令

在 Android 开发中,invoke-super 指令经常被用来重写某个系统方法,同时保留父类方法中的一些逻辑。以 Activity 类的 onCreate 方法为例:

public class MyActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); // 调用父类的 onCreate 方法
    }
}

其中 super.onCreate(savedInstanceState) 就是一种典型的 super 调用,经过 Dex 编译后,在 Smali 语法层面看到的就是 invoke-super 指令。但在 patch 类中,无法编写类似 myActivity.super.onCreate(savedInstanceState),因为 super 只能在原类使用;即使采用字节码技术强行编写了,在运行时会提示 java.lang.NoSuchMethodError,因为 patch 不是目标方法的子类。

为了模拟实现 JVM 的 invoke-super 指令,需要为每个 patch 类生成一个继承了被修复类父类的辅助类(解决 super 调用只能在目标子类使用的问题),并且在辅助类里面将 patch.onCreate 转换为原始类的调用 origin.super.onCreate。Robust 早期是在 Smali 层面进行处理的,需要将 Dex 转换为 Smali,处理完以后,再把 Smali 转换为 Dex。用 ASM 字节码直接对 Class 字节码进行处理更方便,不需要再转换为 Smali,针对该辅助类的ASM字节码转换关键代码如下:

public class SuperMethodVisitor extends MethodVisitor {
    ...
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        if (opcode == Opcodes.INVOKEVIRTUAL) {
            // 将 INVOKEVIRTUAL 指令替换成 INVOKESPECIAL
            super.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, desc, itf);
        } else {
            super.visitMethodInsn(opcode, owner, name, desc, itf);
        }
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
        if (opcode == Opcodes.ALOAD && var == 0) {
            //保证super调用在原始类
            mv.visitVarInsn(opcode, 1);
            return;
        }
        mv.visitVarInsn(opcode, var);
    }
    ...
}

上述方式是采用了一个辅助类来实现的,下面介绍另一种改进的方法。

在 JNI 层,常见的 CallObjectMethod 函数适用于调用虚方法,即调用方法时依赖于对象的类层次结构,类似于 Java 的 invoke-virtual;与之对应的是 CallNonvirtualObjectMethod 函数,它适用于非虚方法调用,即调用的对象为指定的类的对象,无论这个类有没有被继承或覆盖,也就是说可以通过 CallNonvirtualObjectMethod 调用父类 super 方法。

Java 语言中的 invoke-super 指令可以通过 CallNonvirtualObjectMethod、GetMethodID 组合来实现,关键代码如下:

jmethodID methodID = env->GetMethodID(parentClass, "superMethodName", "()V");
jvalue args[] = {};
jobject result = env->CallNonvirtualObjectMethod(parentObj, parentClass, methodID, args);

(2)<init> 函数的插桩与修复

在部分子类 <init> 函数会显式调用父类的构造函数 super() ,且 super() 必须是子类 <init> 函数中的第一句语句,否则编译失败。因此对于 <init> 函数,不能在第一行进行 Robust 插桩,需要在父类的构造函数 super() 之后插桩。

那么 <init> 函数如何修复呢?原始类 <init> 函数修改后,在 patch 类也是 <init> 函数,这里需要将该 <init> 函数拷贝成普通函数,并将原始类的 Robust 插桩关联到该普通函数。

复制构造函数并将其转换为方法需要注意:

  • 原始类函数名称 <init> 需要改成普通方法名称,避免与 patch 类的 <init> 函数冲突。

  • 原始类 <init> 函数如果有方法参数,则需要保留成一致的。

  • patch 类新方法的 return type 是 void。

  • 原始类 <init> 函数如果有调用 this() 或 super() 构造函数,则需要在 patch 新方法里删除它们。

(3)<clinit> 函数的插桩与修复

<clinit> 函数是由编译器生成的一个特殊的静态构造方法,它被用来初始化类中的静态变量和复杂的静态表达式。如果在一个类中定义了静态变量或代码块,那么编译器会为这些静态变量和代码块生成一个 <clinit> 函数。<clinit> 函数只会被执行一次,虚拟机会保证只有一个线程能够执行 <clinit> 方法,确保对共享的类级别变量的线程安全访问。

因此,对 <clinit> 函数进行插桩和修复时,需要特别注意 <clinit> 方法的执行时机:

  • 在类实例化时,如果该类的 <clinit> 方法还没有执行,则会执行该方法,以初始化类的静态变量和复杂的静态表达式。

  • 在通过反射获取该类的某个静态成员时,如果该类的 <clinit> 方法还没有执行,则会执行该方法,以初始化类的静态变量和复杂的静态表达式。

  • 如果该类被子类继承,而子类中也定义了 <clinit> 方法,则在创建子类实例时,会先执行父类的 <clinit> 方法,然后再执行子类的 <clinit> 方法。

根据上述 <clinit> 函数执行时机分析,插桩时不能访问类的静态成员变量(访问静态变量时 clinit 函数就已经执行了,无法被有效修复),因此无法借助于Robust常规插桩方法(给 Class 插入一个静态接口 Field),需要借助一个辅助类 ClintPatchProxy 来实现插桩逻辑。

/**
 * 线上 MainActiviy clinit 插桩
 */
public class MainActivity {
    static {
        String classLongName = "com.app.MainActivity";
        if (ClintPatchProxy.isSupport(classLongName)) {
            ClintPatchProxy.accessDispatch(classLongName);
        } else {
            // MainActitiy Clinit origin code
        }

clinit 函数修复时,在补丁入口类的静态代码块里面设置好 ClintPatchProxy 的跳转接口实现即可,原 MainActivity 的 clinit 代码将不再执行,转而执行 MainActivityPatch的clinit 代码(对应 MainActivity 的新 clinit 代码)。

(4)修复新增类/新增成员变量/新增方法

基于方法插桩的方法,天然支持新增类;对于新增 Field 和新增 Method,分两种情况:静态的 Field 和 Method 可以用一个新增类来包裹;新增非静态 Field 可以使用一个辅助类来维持 this 对象与该 Field 的映射关系,补丁里面原本使用this.newFieldName的代码,通过字节码工具转换为 FieldHelper.get(this).getNewFieldName() 即可。

 4 总结 

回顾 Robust 热修复补丁制作过程,主要是对构建编译过程和字节码编辑技术的巧妙结合。通过分析 Android 应用打包过程、Java 语言编译和优化过程,补丁制作过程中可能会遇到的各种问题就有了答案,再借助字节码工具分析、处理就能够生成一个热修复补丁。

当然,这其中涉及大量的细节处理,仅通过一篇文章不足以涵盖各种细节,还需要结合实际项目才能有更全面的了解。

 5 本文作者 

常强,美团平台-App技术部工程师。

----------  END  ----------

 推荐阅读 

  | Android对so体积优化的探索与实践

  | Android静态代码扫描效率优化与实践

  | Android兼容Java 8语法特性的原理分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/542883.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鄞州银行:符合中小银行质量提升的数据治理方案

案例简介 在数字化转型的驱动和数据治理“严监管”的推动下&#xff0c;为解决金融机构数据治理体系不健全、数据质量低下等问题&#xff0c;利用数据治理成熟度评估模型进行问题分析定位&#xff0c;重点围绕数据规划、组织机制、标准建设以及数据类平台建设等方面进行数据质…

springboot+jsp网上药品商城销售管理系统

本设计需要实现一套方便药品管理者轻松便捷的处理药品运营工作的药品销售管理系统。设计并实现了特殊药品管理系统。系统选用B/S模式&#xff0c;应用java开发语言&#xff0c; MySQL为后台数据库。系统主要包括主页、个人中心、用户管理、药品类别管理、药品信息管理、系统管理…

Boy,Slowly...

很多朋友问我为啥写的少了。我说很多东西都是常识&#xff0c;老生常谈无数遍了&#xff0c;不想不断重复写了。常识性的东西&#xff0c;不断强调是对的&#xff0c;但是不断重复写&#xff0c;这就不对了。 &#xff08;1&#xff09;朴素 早上看一位朋友发了一条王兴过去老生…

使用Python复制某文件夹下子文件夹名为数据文件夹下的所有以DD开头的文件夹到桌面...

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 楼阁玲珑五云起&#xff0c;其中绰约多仙子。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python最强王者群【魏哥】问了一个Python自动化办公处理…

macOS Ventura 13.4 (22F66) 正式版发布,ISO、IPSW、PKG 下载

本站下载的 macOS Ventura 软件包&#xff0c;既可以拖拽到 Applications&#xff08;应用程序&#xff09;下直接安装&#xff0c;也可以制作启动 U 盘安装&#xff0c;或者在虚拟机中启动安装。另外也支持在 Windows 和 Linux 中创建可引导介质。 请访问原文链接&#xff1a…

在 Python 中使用 OpenCV 构建 Color Catcher 游戏

介绍 你是否曾经想在 Python 中使用 OpenCV 创建自己的游戏&#xff1f; 今天我们将构建一个名为 Color Catcher 的游戏&#xff0c;该游戏挑战玩家使用手部跟踪机制接住从屏幕顶部掉落的彩球。 设置游戏窗口 构建游戏的第一步是使用 OpenCV 设置游戏窗口。我们将定义窗口大小、…

Swift静态代码检测工程实践

本文字数&#xff1a;22817字 预计阅读时间&#xff1a;58分钟 引言 随着App功能不断增加&#xff0c;工程代码量也随之快速增加&#xff0c;依靠人工CodeReview来保证项目的质量&#xff0c;越来越不现实&#xff0c;这时就有必要借助于自动化的代码审查工具&#xff0c;进行程…

【运维知识进阶篇】集群架构-Nginx七层负载均衡详解

为什么要使用负载均衡 当我们的Web服务器直接面向用户&#xff0c;往往要承载大量并发请求&#xff0c;单台服务器难以负荷&#xff0c;我使用多台Web服务器组成集群&#xff0c;前端使用Nginx负载均衡&#xff0c;将请求分散的打到我们的后端服务器集群中&#xff0c;实现负载…

音频品鉴与歌唱评价——音频内容理解实践

歌唱评价是K歌系统中核心技术之一。近年来&#xff0c;歌唱评价领域也发生着多元化和深度化的变革。本次LiveVideoStackCon 2022 北京站邀请到腾讯音乐天琴实验室高级研究员——江益靓&#xff0c;为大家介绍全民K歌的多维度评价技术和深度歌唱评价技术的实践&#xff0c;以及优…

Linux系统编程学习 NO.3 ——基础指令的学习

* 通配符 通匹配任意字符&#xff08;包括空字符&#xff09;&#xff0c;用于匹配任意长度的字符串。包括空字符。加入你要匹配任何以.txt后缀的文件&#xff0c;只需要在*通配符后加上.txt后缀即可。 样例演示 ls *.后缀名 ls xxx* 找到匹配的字符串man指令(重要) Linux的…

理论力学专题:张量分析

张量方法的引入 自然法则与坐标无关&#xff0c;坐标系的引入方便分析&#xff0c;但也掩盖了物理本质指标符号哑标和自由标 Einstein求和约定&#xff1a;凡在某一项内&#xff0c;重复一次且仅重复一次的指标&#xff0c;表示对该指标在它的取值范围内求和&#xff0c;并称这…

【C++ 入坑指南】(10)函数

文章目录 简介定义实例函数的分文件编写 简介 函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数&#xff0c;即主函数 main() &#xff0c;所有简单的程序都可以定义其他额外的函数。 您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定…

分享3个深度学习练手的小案例

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

0ctf_2017_babyheap-fastbin_dup_into_stack

参考&#xff1a; [1]https://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html [2]https://blog.csdn.net/qq_43935969/article/details/115877748 [3]https://bbs.kanxue.com/thread-223461.htm 题目下载参考[1] 说明下如何调试堆&#xff0c;在payload中…

chatgpt赋能Python-python3_7如何下载

Python3.7如何下载&#xff1f;详细步骤分享&#xff01; Python是一门当今最热门、最常用、最易学的编程语言之一&#xff0c;且拥有庞大的社区和强大的库支持。在这篇文章中&#xff0c;我们将会详细介绍如何下载Python3.7版本&#xff0c;让大家能够轻松上手Python编程。 …

代码随想录训练营Day44| 完全背包 518. 零钱兑换 II 377. 组合总和 Ⅳ

目录 学习目标 学习内容 完全背包 518. 零钱兑换 II 377. 组合总和 Ⅳ 学习目标 完全背包 518. 零钱兑换 II 377. 组合总和 Ⅳ 学习内容 完全背包 problems/背包问题理论基础完全背包.md programmercarl/leetcode-master&#xff08;代码随想录出品&#xff09; - Git…

chatgpt赋能Python-python3_5怎么打开

Python 3.5怎么打开&#xff1f;教你几种方法 Python是目前非常流行的一种编程语言&#xff0c;几乎在所有行业都得到了广泛的应用。Python非常容易上手&#xff0c;且有强大的数据处理和科学计算能力。现在我们来说一下&#xff0c;如何在您的计算机上打开Python 3.5。 方法…

万劫不复之地-云原生可观测性的几大误区

传统监控厂商正把可观测性引入万劫不复之地 可观测性是当前讨论非常多的话题&#xff0c;这个理念由来已久&#xff0c;却在最近开始流行。 在20世纪60年代&#xff0c;该理念首次由Rudolf E. Kalman在其论文中提出&#xff0c;论文题目是《on a general theory of control s…

自然语言处理基础

以下所有内容来自《自然语言处理 基于预训练模型的方法》 1. 文本的表示 利用计算机对自然语言进行处理&#xff0c;首先要解决语言在计算机内部的存储和计算问题。使用字符串表示计算文本的语义信息的时候&#xff0c;往往使用基于规则的方法。如&#xff1a;判断一个句子编…

一文深度解读机器学习模型的评估方法

我们训练学习好的模型&#xff0c;通过客观地评估模型性能&#xff0c;才能更好实际运用决策。模型评估主要有&#xff1a;预测误差情况、拟合程度、模型稳定性等方面。还有一些场景对于模型预测速度&#xff08;吞吐量&#xff09;、计算资源耗用量、可解释性等也会有要求&…