浅谈Android热更新的前因后果 _ Android

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

首先,我们需要持有以下几个问题:

  1. 什么是热修复?它可以帮我解决什么问题?
  2. 热修复的产生背景?
  3. 热修复的基本原理是什么?
  4. 如何选择热修复框架?
  5. 热修复的注意事项
  6. 热修复与多渠道?
  7. 自动化构建与热修复?

上面一共有7个问题,如果是新同学的话,后面两条可能不会很了解,建议自行补课学习。于是最基本的5个问题,我们必须明白,这是我们每个开发者学习一个新知识的基本需要做到的。

什么是热修复?它可以帮我解决什么问题?

其实简单来说,热修复就是一种动态加载技术,比如你线上某个产品此时出现了bug:

传统流程:debug->测试->发布新版 ->用户安装(各平台审核时间不一,而且用户需要手动下载或者更新)

集成热修复情况下:dubug->测试->推送补丁->自动下载补丁修复 (用户不知情况,自动下载补丁并修复)

对比下来,我们不难发现,传统流程存在这几大弊端:

  1. 发版代价大
  2. 用户下载安装的成本过高
  3. bug修复不及时,取决于各平台的审核时间等等

热修复产生背景?

  • app发版成本高
  • 用H5集成某些经常变动的业务逻辑,但这种方案需要学习成本,而且对于无法转为H5形式的代码仍旧是无法修复;
  • Instant Run

上面三个原因中,我们主要来谈一下 Instant Run:

Android Studio2.0时,新增了一个 Instant Run的功能,而各大厂的热修复方案,在代码,资源等方面的实现都是很大程度上参考了Instant Run的代码。所以可以说 Instant Run 是推进Android 热修复的主因。

那 Instant Run内部是如何做到这一点呢?

  1. 构建一个新的 AssetManager(资源管理框架),并通过反射调用这个 addAssetPath,把这个完整的新资源加入到 AssetManager中,这样就得到了一个含有所有新资源的 AssetManager.
  2. 找到所有之前引用到原有AssetManager的地方,通过反射,把引用出替换为新的AssetManager.

参考自 <深入探索Android热修复技术原理>

关于 InstantRun 的更多解释请参考:

热修复的原理是什么?

我们都知道热修复都相当于动态加载,那么动态加载到底动态在哪里了呢。

说到这个就躲不过一个关键点 ClassLoader(类加载器) ,所以我们先从Java开始。

image-20191124213728766

我们都知道Java的类加载器有四种,分别为:

  • Bootstarp ClassLoader
  • Extension ClassLoader
  • App ClassLoader 加载应用ClassLoader
  • Custom ClassLoader 加载自己的class文件

类加载过程如下:

过程: 加载-连接(验证-准备-解析)-初始化

  1. 加载

    将类的信息(字节码)从文件中获取并载入到JVM的内存中

  2. 连接

    验证:检查读入的结构是否符合JVM规范

    准备:分配一个结构来存储类的信息

    解析:将类的常量池中的所有引用改变成直接引用

  3. 初始化

    执行静态初始化程序,把静态变量初始化成指定的值

其中用到的三个主要机制:

  1. 双亲委托机制
  2. 全盘负责机制
  3. 缓存机制

其实后面的两个机制都是主要从双亲委托机制延续而来。详细的Java类加载请参考我的另一篇博客

在说明了Java 的ClassLoader之后,我们接下来开始Android的ClassLoader,不同于Java的是,Java中的ClassLoader可以加载 jar 文件和 Class文件,而Android中加载的是Dex文件,这就需要重新设计相关的ClassLoader类。所以Android 的ClassLoader 我们会说的详细一点

image-20191125191625071

源码解析

在这里,顺便提一下,这里贴的代码版本是Android 9.0,在8.0以后,PathClassLoader和DexClassLoader并没有什么区别,因为唯一的一个区别参数 optimizedDirectory已经被废弃。

首先是 loadClass,也就是我们类加载的核心方法方法:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded //查找当前类是否被加载过 Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//查看父加载器是否加载过
c = parent.loadClass(name, false);
} else {
//如果没有加载过,调用根加载器加载,双亲委托模式的实现
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

//找到根加载器依然为null,只能自己加载了
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}

这里有个问题,JVM双亲委托机制可以被打破吗?先保留疑问。

我们主要去看他的 findClass方法

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

这个方法是一个null实现,也就是需要我们开发者自己去做。

从上面基础我们知道,在Android中,是有 PathClassLoader和 DexClassLoader,而它们又都继承与 BaseDexClassLoader,而这个BaseDexClassLoader又继承与 ClassLoader,并将findClass方法交给子类自己实现,所以我们从它的两个子类 PathClassLoader和 DexClassLoader入手,看看它们是怎么处理的。

这里碍于Android Studio无法查看相关具体实现源码,所以我们从源码网站上查询:

PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {

public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}

// dexPath: 需要加载的文件列表,文件可以是包含了 classes.dex 的 JAR/APK/ZIP,也可以直接使用 classes.dex 文件,多个文件用 “:” 分割
// librarySearchPath: 存放需要加载的 native 库的目录
// parent: 父 ClassLoader
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

由注释看可以发现PathClassLoader被用来加载本地文件系统上的文件或目录,因为它调用的 BaseDexClassLoader的第二个参数为null,即未传入优化后的Dex文件。

注意:Android 8.0之后,BaseClassLoader第二个参数为(optimizedDirectory)为null,所以DexClassLoader与PathClassLoader并无区别

DexClassLoader

image-20191121000739855

public class DexClassLoader extends BaseDexClassLoader {
// dexPath: 需要加载的文件列表,文件可以是包含了 classes.dex 的 JAR/APK/ZIP,也可以直接使用 classes.dex 文件,多个文件用 “:” 分割
// optimizedDirectory: 存放优化后的 dex,可以为空
// librarySearchPath: 存放需要加载的 native 库的目录
// parent: 父 ClassLoader
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

DexClassLoader用来加载jar、apk,其实还包括zip文件或者直接加载dex文件,它可以被用来执行未安装的代码或者未被应用加载过的代码,也就是我们修复过的代码。

注意:Android 8.0之后,BaseClassLoader第二个参数为(optimizedDirectory)为null,所以DexClassLoader与PathClassLoader并无区别

从上面我们可以看到,它们都继承于BaseDexClassLoader,并且它们真正的实现行为都是调用的父类方法,所以我们来看一下BaseDexClassLoader.

BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {

private static volatile Reporter reporter = null;

//核心关注点
private final DexPathList pathList;

BaseDexClassLoader 构造函数有四个参数,含义如下:

// dexPath: 需要加载的文件列表,文件可以是包含了 classes.dex 的 JAR/APK/ZIP,也可以直接使用 classes.dex 文件,多个文件用 “:” 分割
// optimizedDirectory: 存放优化后的 dex,可以为空
// librarySearchPath: 存放需要加载的 native 库的目录
// parent: 父 ClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
//classloader,dex路径,目录列表,内部文件夹
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

if (reporter != null) {
reportClassLoaderChain();
}
}

public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}

//核心方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//异常处理
List suppressedExceptions = new ArrayList();
//这里也只是一个中转,关注点在 DexPathList
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn’t find class “” + name + “” on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}


}

从上面我们可以发现,BaseDexClassLoader其实也不是主要处理的类,所以我们继续去查找 DexPathList.

DexPathList

final class DexPathList {
//文件后缀
private static final String DEX_SUFFIX = “.dex”;
private static final String zipSeparator = “!/”;

** class definition context */
private final ClassLoader definingContext;

//内部类 Element
private Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}

DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
if (definingContext == null) {
throw new NullPointerException(“definingContext == null”);
}

if (dexPath == null) {
throw new NullPointerException(“dexPath == null”);
}

if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn’t exist: "

  • optimizedDirectory);
    }

if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "

  • optimizedDirectory);
    }
    }

this.definingContext = definingContext;

ArrayList suppressedExceptions = new ArrayList();
// save dexPath for BaseDexClassLoader
//我们关注这个 makeDexElements 方法
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty(“java.library.path”), true);
List allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);

if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}

static class Element {
//dex文件为null时表示 jar/dex.jar文件
private final File path;

//android虚拟机文件在Android中的一个具体实现
private final DexFile dexFile;

private ClassPathURLStreamHandler urlHandler;
private boolean initialized;

/**

  • Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
  • should be null), or a jar (in which case dexZipPath should denote the zip file).
    */
    public Element(DexFile dexFile, File dexZipPath) {
    this.dexFile = dexFile;
    this.path = dexZipPath;
    }

public Element(DexFile dexFile) {
this.dexFile = dexFile;
this.path = null;
}

public Element(File path) {
this.path = path;
this.dexFile = null;
}

public Class<?> findClass(String name, ClassLoader definingContext,
List suppressed) {
//核心点,DexFile
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}

/**

  • Constructor for a bit of backwards compatibility. Some apps use reflection into
  • internal APIs. Warn, and emulate old behavior if we can. See b/33399341.
  • @deprecated The Element class has been split. Use new Element constructors for
  •         classes and resources, and NativeLibraryElement for the library
    
  •         search path.
    

*/
@Deprecated
public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
System.err.println(“Warning: Using deprecated Element constructor. Do not use internal”

  • " APIs, this constructor will be removed in the future.");
    if (dir != null && (zip != null || dexFile != null)) {
    throw new IllegalArgumentException(“Using dir and zip|dexFile no longer”
  • " supported.");
    }
    if (isDirectory && (zip != null || dexFile != null)) {
    throw new IllegalArgumentException(“Unsupported argument combination.”);
    }
    if (dir != null) {
    this.path = dir;
    this.dexFile = null;
    } else {
    this.path = zip;
    this.dexFile = dexFile;
    }
    }

    }


//主要作用就是将 我们指定路径中所有文件转化为DexFile,同时存到Eelement数组中
//为什么要这样做?目的就是为了让findClass去实现
private static Element[] makeDexElements(List files, File optimizedDirectory,
List suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
//遍历所有文件
for (File file : files) {
if (file.isDirectory()) {
//如果存在文件夹,查找文件夹内部查询
elements[elementsPos++] = new Element(file);
//如果是文件
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
//判断是否是dex文件
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
//创建一个DexFile
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/*

  • IOException might get thrown “legitimately” by the DexFile constructor if
  • the zip file turns out to be resource-only (that is, no classes.dex file
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

我坚信,坚持学习,每天进步一点,滴水穿石,我们离成功都很近!
以下是总结出来的字节经典面试题目,包含:计算机网络,Kotlin,数据结构与算法,Framework源码,微信小程序,NDK音视频开发,计算机网络等。

字节高级Android经典面试题和答案


《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

内容对你有帮助,可以扫码获取!!(备注:Android)**

最后

我坚信,坚持学习,每天进步一点,滴水穿石,我们离成功都很近!
以下是总结出来的字节经典面试题目,包含:计算机网络,Kotlin,数据结构与算法,Framework源码,微信小程序,NDK音视频开发,计算机网络等。

字节高级Android经典面试题和答案

[外链图片转存中…(img-2SzO2CcP-1713677187635)]
[外链图片转存中…(img-9ebu7MUJ-1713677187636)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值