前言

说起热修复,已经是目前Android开发必备技能。我所了解的一种实现方式就是类加载方案,即 dex 插桩,这种思路在插件化中也会用到。除此之外,还有底层替换方案,即修改替换 ArtMethod。采用类加载方案的主要是以腾讯系为主,包括微信的 Tinker、饿了么的 Amigo;采用底层替换方案主要是阿里系的 AndFix 等。今天我将围绕热修复实现原理以及常见的热修复方式来讲解热修复。

目录

#mermaid-svg-m13gMJVqHyoxE0dX .label{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);fill:#333;color:#333}#mermaid-svg-m13gMJVqHyoxE0dX .label text{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .node rect,#mermaid-svg-m13gMJVqHyoxE0dX .node circle,#mermaid-svg-m13gMJVqHyoxE0dX .node ellipse,#mermaid-svg-m13gMJVqHyoxE0dX .node polygon,#mermaid-svg-m13gMJVqHyoxE0dX .node path{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-m13gMJVqHyoxE0dX .node .label{text-align:center;fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .node.clickable{cursor:pointer}#mermaid-svg-m13gMJVqHyoxE0dX .arrowheadPath{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-svg-m13gMJVqHyoxE0dX .flowchart-link{stroke:#333;fill:none}#mermaid-svg-m13gMJVqHyoxE0dX .edgeLabel{background-color:#e8e8e8;text-align:center}#mermaid-svg-m13gMJVqHyoxE0dX .edgeLabel rect{opacity:0.9}#mermaid-svg-m13gMJVqHyoxE0dX .edgeLabel span{color:#333}#mermaid-svg-m13gMJVqHyoxE0dX .cluster rect{fill:#ffffde;stroke:#aa3;stroke-width:1px}#mermaid-svg-m13gMJVqHyoxE0dX .cluster text{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-svg-m13gMJVqHyoxE0dX .actor{stroke:#ccf;fill:#ECECFF}#mermaid-svg-m13gMJVqHyoxE0dX text.actor>tspan{fill:#000;stroke:none}#mermaid-svg-m13gMJVqHyoxE0dX .actor-line{stroke:grey}#mermaid-svg-m13gMJVqHyoxE0dX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333}#mermaid-svg-m13gMJVqHyoxE0dX .messageLine1{stroke-width:1.5;stroke-dasharray:2, 2;stroke:#333}#mermaid-svg-m13gMJVqHyoxE0dX #arrowhead path{fill:#333;stroke:#333}#mermaid-svg-m13gMJVqHyoxE0dX .sequenceNumber{fill:#fff}#mermaid-svg-m13gMJVqHyoxE0dX #sequencenumber{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX #crosshead path{fill:#333;stroke:#333}#mermaid-svg-m13gMJVqHyoxE0dX .messageText{fill:#333;stroke:#333}#mermaid-svg-m13gMJVqHyoxE0dX .labelBox{stroke:#ccf;fill:#ECECFF}#mermaid-svg-m13gMJVqHyoxE0dX .labelText,#mermaid-svg-m13gMJVqHyoxE0dX .labelText>tspan{fill:#000;stroke:none}#mermaid-svg-m13gMJVqHyoxE0dX .loopText,#mermaid-svg-m13gMJVqHyoxE0dX .loopText>tspan{fill:#000;stroke:none}#mermaid-svg-m13gMJVqHyoxE0dX .loopLine{stroke-width:2px;stroke-dasharray:2, 2;stroke:#ccf;fill:#ccf}#mermaid-svg-m13gMJVqHyoxE0dX .note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-m13gMJVqHyoxE0dX .noteText,#mermaid-svg-m13gMJVqHyoxE0dX .noteText>tspan{fill:#000;stroke:none}#mermaid-svg-m13gMJVqHyoxE0dX .activation0{fill:#f4f4f4;stroke:#666}#mermaid-svg-m13gMJVqHyoxE0dX .activation1{fill:#f4f4f4;stroke:#666}#mermaid-svg-m13gMJVqHyoxE0dX .activation2{fill:#f4f4f4;stroke:#666}#mermaid-svg-m13gMJVqHyoxE0dX .mermaid-main-font{font-family:"trebuchet ms", verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .section{stroke:none;opacity:0.2}#mermaid-svg-m13gMJVqHyoxE0dX .section0{fill:rgba(102,102,255,0.49)}#mermaid-svg-m13gMJVqHyoxE0dX .section2{fill:#fff400}#mermaid-svg-m13gMJVqHyoxE0dX .section1,#mermaid-svg-m13gMJVqHyoxE0dX .section3{fill:#fff;opacity:0.2}#mermaid-svg-m13gMJVqHyoxE0dX .sectionTitle0{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .sectionTitle1{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .sectionTitle2{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .sectionTitle3{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .sectionTitle{text-anchor:start;font-size:11px;text-height:14px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .grid .tick{stroke:#d3d3d3;opacity:0.8;shape-rendering:crispEdges}#mermaid-svg-m13gMJVqHyoxE0dX .grid .tick text{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .grid path{stroke-width:0}#mermaid-svg-m13gMJVqHyoxE0dX .today{fill:none;stroke:red;stroke-width:2px}#mermaid-svg-m13gMJVqHyoxE0dX .task{stroke-width:2}#mermaid-svg-m13gMJVqHyoxE0dX .taskText{text-anchor:middle;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .taskText:not([font-size]){font-size:11px}#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-svg-m13gMJVqHyoxE0dX .task.clickable{cursor:pointer}#mermaid-svg-m13gMJVqHyoxE0dX .taskText.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-m13gMJVqHyoxE0dX .taskText0,#mermaid-svg-m13gMJVqHyoxE0dX .taskText1,#mermaid-svg-m13gMJVqHyoxE0dX .taskText2,#mermaid-svg-m13gMJVqHyoxE0dX .taskText3{fill:#fff}#mermaid-svg-m13gMJVqHyoxE0dX .task0,#mermaid-svg-m13gMJVqHyoxE0dX .task1,#mermaid-svg-m13gMJVqHyoxE0dX .task2,#mermaid-svg-m13gMJVqHyoxE0dX .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutside0,#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutside2{fill:#000}#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutside1,#mermaid-svg-m13gMJVqHyoxE0dX .taskTextOutside3{fill:#000}#mermaid-svg-m13gMJVqHyoxE0dX .active0,#mermaid-svg-m13gMJVqHyoxE0dX .active1,#mermaid-svg-m13gMJVqHyoxE0dX .active2,#mermaid-svg-m13gMJVqHyoxE0dX .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-svg-m13gMJVqHyoxE0dX .activeText0,#mermaid-svg-m13gMJVqHyoxE0dX .activeText1,#mermaid-svg-m13gMJVqHyoxE0dX .activeText2,#mermaid-svg-m13gMJVqHyoxE0dX .activeText3{fill:#000 !important}#mermaid-svg-m13gMJVqHyoxE0dX .done0,#mermaid-svg-m13gMJVqHyoxE0dX .done1,#mermaid-svg-m13gMJVqHyoxE0dX .done2,#mermaid-svg-m13gMJVqHyoxE0dX .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-svg-m13gMJVqHyoxE0dX .doneText0,#mermaid-svg-m13gMJVqHyoxE0dX .doneText1,#mermaid-svg-m13gMJVqHyoxE0dX .doneText2,#mermaid-svg-m13gMJVqHyoxE0dX .doneText3{fill:#000 !important}#mermaid-svg-m13gMJVqHyoxE0dX .crit0,#mermaid-svg-m13gMJVqHyoxE0dX .crit1,#mermaid-svg-m13gMJVqHyoxE0dX .crit2,#mermaid-svg-m13gMJVqHyoxE0dX .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-svg-m13gMJVqHyoxE0dX .activeCrit0,#mermaid-svg-m13gMJVqHyoxE0dX .activeCrit1,#mermaid-svg-m13gMJVqHyoxE0dX .activeCrit2,#mermaid-svg-m13gMJVqHyoxE0dX .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-svg-m13gMJVqHyoxE0dX .doneCrit0,#mermaid-svg-m13gMJVqHyoxE0dX .doneCrit1,#mermaid-svg-m13gMJVqHyoxE0dX .doneCrit2,#mermaid-svg-m13gMJVqHyoxE0dX .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-svg-m13gMJVqHyoxE0dX .milestone{transform:rotate(45deg) scale(0.8, 0.8)}#mermaid-svg-m13gMJVqHyoxE0dX .milestoneText{font-style:italic}#mermaid-svg-m13gMJVqHyoxE0dX .doneCritText0,#mermaid-svg-m13gMJVqHyoxE0dX .doneCritText1,#mermaid-svg-m13gMJVqHyoxE0dX .doneCritText2,#mermaid-svg-m13gMJVqHyoxE0dX .doneCritText3{fill:#000 !important}#mermaid-svg-m13gMJVqHyoxE0dX .activeCritText0,#mermaid-svg-m13gMJVqHyoxE0dX .activeCritText1,#mermaid-svg-m13gMJVqHyoxE0dX .activeCritText2,#mermaid-svg-m13gMJVqHyoxE0dX .activeCritText3{fill:#000 !important}#mermaid-svg-m13gMJVqHyoxE0dX .titleText{text-anchor:middle;font-size:18px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX g.classGroup text{fill:#9370db;stroke:none;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:10px}#mermaid-svg-m13gMJVqHyoxE0dX g.classGroup text .title{font-weight:bolder}#mermaid-svg-m13gMJVqHyoxE0dX g.clickable{cursor:pointer}#mermaid-svg-m13gMJVqHyoxE0dX g.classGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-m13gMJVqHyoxE0dX g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5}#mermaid-svg-m13gMJVqHyoxE0dX .classLabel .label{fill:#9370db;font-size:10px}#mermaid-svg-m13gMJVqHyoxE0dX .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-m13gMJVqHyoxE0dX .dashed-line{stroke-dasharray:3}#mermaid-svg-m13gMJVqHyoxE0dX #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX #compositionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX #aggregationStart{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX #aggregationEnd{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX #dependencyStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX #dependencyEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX #extensionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX .commit-id,#mermaid-svg-m13gMJVqHyoxE0dX .commit-msg,#mermaid-svg-m13gMJVqHyoxE0dX .branch-label{fill:lightgrey;color:lightgrey;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .pieTitleText{text-anchor:middle;font-size:25px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .slice{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX g.stateGroup text{fill:#9370db;stroke:none;font-size:10px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX g.stateGroup text{fill:#9370db;fill:#333;stroke:none;font-size:10px}#mermaid-svg-m13gMJVqHyoxE0dX g.statediagram-cluster .cluster-label text{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX g.stateGroup .state-title{font-weight:bolder;fill:#000}#mermaid-svg-m13gMJVqHyoxE0dX g.stateGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-m13gMJVqHyoxE0dX g.stateGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-m13gMJVqHyoxE0dX .transition{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-m13gMJVqHyoxE0dX .stateGroup .composit{fill:white;border-bottom:1px}#mermaid-svg-m13gMJVqHyoxE0dX .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px}#mermaid-svg-m13gMJVqHyoxE0dX .state-note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-m13gMJVqHyoxE0dX .state-note text{fill:black;stroke:none;font-size:10px}#mermaid-svg-m13gMJVqHyoxE0dX .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.7}#mermaid-svg-m13gMJVqHyoxE0dX .edgeLabel text{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .stateLabel text{fill:#000;font-size:10px;font-weight:bold;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-m13gMJVqHyoxE0dX .node circle.state-start{fill:black;stroke:black}#mermaid-svg-m13gMJVqHyoxE0dX .node circle.state-end{fill:black;stroke:white;stroke-width:1.5}#mermaid-svg-m13gMJVqHyoxE0dX #statediagram-barbEnd{fill:#9370db}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-cluster rect{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-cluster rect.outer{rx:5px;ry:5px}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-state .divider{stroke:#9370db}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-state .title-state{rx:5px;ry:5px}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-cluster.statediagram-cluster .inner{fill:white}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-cluster.statediagram-cluster-alt .inner{fill:#e0e0e0}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-cluster .inner{rx:0;ry:0}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-state rect.basic{rx:5px;ry:5px}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#efefef}#mermaid-svg-m13gMJVqHyoxE0dX .note-edge{stroke-dasharray:5}#mermaid-svg-m13gMJVqHyoxE0dX .statediagram-note rect{fill:#fff5ad;stroke:#aa3;stroke-width:1px;rx:0;ry:0}:root{--mermaid-font-family: '"trebuchet ms", verdana, arial';--mermaid-font-family: "Comic Sans MS", "Comic Sans", cursive}#mermaid-svg-m13gMJVqHyoxE0dX .error-icon{fill:#522}#mermaid-svg-m13gMJVqHyoxE0dX .error-text{fill:#522;stroke:#522}#mermaid-svg-m13gMJVqHyoxE0dX .edge-thickness-normal{stroke-width:2px}#mermaid-svg-m13gMJVqHyoxE0dX .edge-thickness-thick{stroke-width:3.5px}#mermaid-svg-m13gMJVqHyoxE0dX .edge-pattern-solid{stroke-dasharray:0}#mermaid-svg-m13gMJVqHyoxE0dX .edge-pattern-dashed{stroke-dasharray:3}#mermaid-svg-m13gMJVqHyoxE0dX .edge-pattern-dotted{stroke-dasharray:2}#mermaid-svg-m13gMJVqHyoxE0dX .marker{fill:#333}#mermaid-svg-m13gMJVqHyoxE0dX .marker.cross{stroke:#333}:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}#mermaid-svg-m13gMJVqHyoxE0dX {color: rgba(0, 0, 0, 0.75);font: ;}

热修复
热修复的应用场景
认识Java类的加载机制-双亲委派模型
Android运行流程
认识Android中的classload
认识Element集合之DexPathList
热修复的原理
热修复的实现
常用第三方热修复组件
总结

热修复的应用场景

热修复就是在APP上线以后,如果突然发现有缺陷了,如果重新走发布流程可能时间比较长,重新安装APP用户体验也不会太好;热修复就是通过发布一个插件,使APP运行的时候加载插件里面的代码,从而解决缺陷,并且对于用户来说是无感的(用户也可能需要重启一下APP)。

认识Java类的加载机制(双亲委派模型)

Java负责加载class文件的就是类加载器(ClassLoader),APP启动的时候,会创建一个自己的ClassLoader实例,我们可以通过下面的代码拿到当前的ClassLoader

ClassLoader classLoader = getClassLoader();
Log.i(TAG, "[onCreate] classLoader" + ":" + classLoader.toString());

然后我们在看一下构造函数。在ClassLoader 这个类中的 loadClass() 方法,它调用的是另一个2个参数的重载 loadClass() 方法。

public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}

我们点进去深入看一下loadClass这个方法:

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// First, check if the class has already been loadedClass<?> 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}if (c == null) {// If still not found, then invoke findClass in order// to find the class.c = findClass(name);}}return c;}

通过分析loadClass方法,你会发现,ClassLoader加载类的方法就是loadClass,是通过双亲委派模型(Parents Delegation Model)实现类的加载的。既在加载一个字节码文件时,会询问当前的classLoader是否已经加载过此字节码文件。如果加载过,则直接返回,不再重复加载。如果没有加载过,则会询问它的Parent是否已经加载过此字节码文件,同样的,如果已经加载过,就直接返回parent加载过的字节码文件,而如果整个继承线路上的classLoader都没有加载过,才由child类加载器(即,当前的子classLoader)执行类的加载工作。整个流程大致可以归纳成如下三步

1. 加载流程

  1. 检查当前的 classLoader 是否已经加载琮这个 class ,有则直接返回,没有则进行第2步。
  2. 调用父 classLoaderloadClass() 方法,检查父 classLoader 是否有加载过这个 class ,有则直
    接返回,没有就继续检查上上个父 classLoader ,直到顶层 classLoader
  3. 如果所有的父 classLoader 都没有加载过这个 class ,则最终由当前 classLoader 调用
    findClass() 方法,去dex文件中找出并加载这个 class

2. 优点

而采用这种类的加载机制的优点就是如果一个类被`classLoader`继承线路上的任意一个加载器加载过,后续在整个系统的生命周期中,这个类都不会再被加载,大大提高了类的加载效率。

3. 作用

  • 类加载的共享功能
    一些Framework层级的类一旦被顶层,classLoader加载过,会缓存到内存中,以后在任何地方用到,都不会去重新加载。大大提高了效率。
  • 类加载的隔离功能
    不同继承线路上的 classLoader 加载的类,肯定不是同一个类,这样可以避免某些开发者自己去写一
    些代码冒充核心类库,来访问核心类库中可见的成员变量。如 java.lang.String 在应用程序启动前就
    已经被系统加载好了,如果在一个应用中能够简单的用自定义的String类把系统中的String类替换掉
    的话,会有严重的安全问题。

Android运行流程

Android运行流程简单来讲大致可以分成如下四步:

  1. Android程序编译的时候,会将.java文件编译时.class文件
  2. 然后将.class文件打包为.dex文件
  3. 然后Android程序运行的时候,AndroidDalvik/ART虚拟机就加载.dex文件
  4. 加载其中的.class文件到内存中来使用

认识Android中的classload

我们知道,AndroidJava有很深的渊源。基于jvmJava应用是通过ClassLoader对象来加载应用中的class的。
AndroidJava的基础上,对jvm又做一层优化和封装。既采用的是dalvik虚拟机。类文件将被打包成dex文件。底层的虚拟机是不同的,所以它们的类加载器当然也会不同。

而常见的Android类加载器有如下四种,下面我们一一讲解这四种。

  1. BootClassLoader :加载Android Framework层中的class字节码文件(类似javaBootstrap
    ClassLoader
  2. PathClassLoader :加载已经安装到系统中的Apkclass 字节码文件(类似javaApp
    ClassLoader
  3. DexClassLoader :加载制定目录的class字节码文件(类似java中的 Custom ClassLoader
  4. BaseDexClassLoaderPathClassLoaderDexClassLoader 的父类

而我们开发的APP一定会用到BootClassLoaderPathClassLoader这2个类加载器,可通过如下代码进行
验证:

    override fun initView(savedInstanceState: Bundle?) {setContentView(binding.root)var classLoader = classLoaderif (classLoader != null) {Log.e(TAG, "myclassLoader = $classLoader")while (classLoader!!.parent != null) {classLoader = classLoader.parentLog.e(TAG, "myclassLoader = $classLoader")}}}

查看一下运行的日志信息如下:

2021-09-08 16:14:49.334 10117-10117/com.bnd.andserver.sample.debug E/com.bnd.andserver.sample.MainActivity: myclassLoader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.bnd.andserver.sample.debug-1/base.apk"],nativeLibraryDirectories=[/data/app/com.bnd.andserver.sample.debug-1/lib/arm64, /data/app/com.bnd.andserver.sample.debug-1/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64]]]
2021-09-08 16:14:49.334 10117-10117/com.bnd.andserver.sample.debug E/com.bnd.andserver.sample.MainActivity: myclassLoader = java.lang.BootClassLoader@43d3385

通过日志和上面代码,我们可以知道,可以通过上下文拿到当前类的类加载器( PathClassLoader ),然后通过getParent()得到父类加载器( BootClassLoader ),这是由于Android中的类加载器和java类加载器一样使用的
是双亲委派模型。

我们知道,Android studio不是所有源码都可以查看的,但是,通过查看一些简单的构造函数还是可以粗滤获知他们的关系的。下面给出可以查看BaseDexClassLoader,PathClassLoader,DexClassLoader的部分代码(并非最终源码)

DexClassLoader 代码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package dalvik.system;import java.io.File;public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {super((String)null, (File)null, (String)null, (ClassLoader)null);throw new RuntimeException("Stub!");}
}

PathClassLoader 代码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package dalvik.system;import java.io.File;public class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super((String)null, (File)null, (String)null, (ClassLoader)null);throw new RuntimeException("Stub!");}public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super((String)null, (File)null, (String)null, (ClassLoader)null);throw new RuntimeException("Stub!");}
}

BaseDexClassLoader 代码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package dalvik.system;import java.io.File;
import java.net.URL;
import java.util.Enumeration;public class BaseDexClassLoader extends ClassLoader {public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {throw new RuntimeException("Stub!");}protected Class<?> findClass(String name) throws ClassNotFoundException {throw new RuntimeException("Stub!");}protected URL findResource(String name) {throw new RuntimeException("Stub!");}protected Enumeration<URL> findResources(String name) {throw new RuntimeException("Stub!");}public String findLibrary(String name) {throw new RuntimeException("Stub!");}protected synchronized Package getPackage(String name) {throw new RuntimeException("Stub!");}public String toString() {throw new RuntimeException("Stub!");}
}

通过初步分析你会发现,PathClassLoader,DexClassLoader都是继承自BaseDexClassLoader ,而BaseDexClassLoader 又是继承自ClassLoader,下面我们就一层一层分析。

认识PathClassLoader和DexClassLoader

PathClassLoaderDexClassLoader的源码都是属于系统级别的,我们无法在开发工具里面查看,有兴趣的同学可以研究一下Android的源码。这里主要介绍一下他们的使用场景

先来介绍一下这两种Classloader在使用场景上的区别

1. 使用场景的区别

1:PathClassLoader :只能加载已经安装到Android系统中的apk文件(/data/app目录),是
Android默认使用的类加载器.
2:DexClassLoader :可以加载任意目录下的dex/jar/apk/zip文件,比 PathClassLoader 更灵活,是
实现热修复的重点。

2. 代码层面的差别
DexClassLoader 代码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package dalvik.system;import java.io.File;public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {super((String)null, (File)null, (String)null, (ClassLoader)null);throw new RuntimeException("Stub!");}
}

PathClassLoader 代码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package dalvik.system;import java.io.File;public class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super((String)null, (File)null, (String)null, (ClassLoader)null);throw new RuntimeException("Stub!");}public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super((String)null, (File)null, (String)null, (ClassLoader)null);throw new RuntimeException("Stub!");}
}

你会通过对比你会发现,PathClassLoaderDexClassLoader 都继承于 BaseDexClassLoaderPathClassLoaderDexClassLoader 在构造函数中都调用了父类的构造函数,但 DexClassLoader
传了一个 optimizedDirectory

认识BaseDexClassLoader

通过观察 PathClassLoaderDexClassLoader 的源码我们就可以确定,真正有意义的处理逻辑肯定是在 BaseDexClassLoader 中,所以下面着重分析 BaseDexClassLoader 源码。下面是一个构造方法

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {super(parent);this.originalPath = dexPath;this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

参数说明:

  • dexPath:要加载的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目录。
  • optimizedDirectory :dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会
    解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的。
  • libraryPath :加载程序文件时需要用到的库路径。
  • parent :父加载器

注意:对于一个完整App的来说,程序文件指定的就是apk包中的 classes.dex 文件;但从热修 复的角度来看,程序文件指的是补丁。因为PathClassLoader只会加载已安装包中已经的dex文件,而DexClassLoader不仅仅可以加载 dex文件,还可以加载jar、apk、zip文件中的dex。而jar、apk、zip其实就是一些压缩格式,要拿到压缩包里面的dex文件就需要解压,所以,DexClassLoader在调用父类构造函数时会指定一个解压的目录。

类加载器肯定会提供有一个方法来供外界找到它所加载到的class,该方法就是 findClass() ,不过在
PathClassLoaderDexClassLoader 源码中都没有重写父类的 findClass() 方法,但它们的父类
BaseDexClassLoader就有重写 findClass() ,所以来看看 BaseDexClassLoaderfindClass() 方法都做了哪些操作,代码如下:

private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();// 实质是通过pathList的对象findClass()方法来获取classClass c = pathList.findClass(name, suppressedExceptions);if (c == null) {ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + namefor (Throwable t : suppressedExceptions) {cnfe.addSuppressed(t);}throw cnfe;}return c;
}

可以看到, BaseDexClassLoaderfindClass() 方法实际上是通过 DexPathListfindClass() 方法来
获取class的,而这个 DexPathList 对象恰好在之前的 BaseDexClassLoader 构造函数中就已经被创建
好了,里面解析了dex文件的路径,并将解析的dex文件都存在this.dexElements里面。所以,下面就来看看 DexPathList 类中都做了什么。

认识Element集合之DexPathList

好了,直接先看DexPathList构造函数:


public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {…//将解析的dex文件都存在this.dexElements里面this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);this.dexElements = makeDexElements(splitDexPath(dexPath),      optimizedDirectory,suppressedExceptions)
}

这个构造函数中,保存了当前的类加载器 definingContext ,并调用了 makeDexElements() 得到 Element 集合。

通过对splitDexPath(dexPath)源码的追踪,发现该方法的作用其实就是将dexPath目录下的所有程序文件转变成一个File集合。同时,dexPath是一个用冒号(":")作为分隔符把多个程序文件目录拼接起来的字符串,比如(如:/data/dexdir1:/data/dexdir2:…)。

接下来在分析 makeDexElements() 方法:

 //解析dex文件
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<I// 1.创建Element集合ArrayList<Element> elements = new ArrayList<Element>();// 2.遍历所有dex文件(也可能是jar、apk或zip文件)for (File file : files) {ZipFile zip = null;DexFile dex = null;String name = file.getName();...// 如果是dex文件if (name.endsWith(DEX_SUFFIX)) {dex = loadDexFile(file, optimizedDirectory);// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)} else {zip = file;dex = loadDexFile(file, optimizedDirectory);}...// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中if ((zip != null) || (dex != null)) {elements.add(new Element(file, false, zip, dex));}}// 4.将Element集合转成Element数组返回return elements.toArray(new Element[elements.size()]);
}

通过分析DexPathListmakeDexElements方法,你会发现,DexPathList 的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)封装成一个个 Element 对象,最后添加到Element集合中。

其实,Android的类加载器(不管是PathClassLoader,还是DexClassLoader,它们最后在加载文件时,都是只认dex文件,而loadDexFile()是加载dex文件的核心方法,他可以可以从jarapkzip中提取出dex

然后我们再回头看一下ClassLoade()加载类的方法,就是loadClass(),最后调用findClass方法完成的;而DexPathList也是重写findClass()方法。如下:

 @Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();// 使用pathList对象查找name类Class c = pathList.findClass(name, suppressedExceptions);return c;}

最终是调用 pathListfindClass方法,看一下方法如下:

public Class findClass(String name, List<Throwable> suppressed) {// 遍历从dexPath查询到的dex和资源Elementfor (Element element : dexElements) {DexFile dex = element.dexFile;// 如果当前的Element是dex文件元素if (dex != null) {// 使用DexFile.loadClassBinaryName加载类Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;
}

其实 DexPathListfindClass() 方法很简单,就只是对 Element 数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个 class ,找不到则返回null

而采用DexFileloadClassBinaryName()方法来加载class,是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件。这可以从Element这个类的源码和dex文件的内部结构看出。

通过如上分析,我们发现整个类加载流程就是:

1:类加载器BaseDexClassLoader先将dex文件解析放到pathListdexElements里面
2:加载类的时候从dexElements里面去遍历,看哪个dex里面有这个类就去加载,生成class对象

所以我们可以将自己的dex文件加载到dexElements里面,并且放在前面,加载的时候就可以加载我们插件中的类,不会加载后面的,从而替换掉原来的class

热修复的原理

通过上面一系列分析,我们已经知道了热修复的实现原理:

热修复的原理就是将补丁 dex 文件放到 dexElements 数组靠前位置,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的

明白了Android类的加载机制和实现原理,接下来就是热修复的实现了。

热修复的实现

知道了原理,实现就比较简单了,就添加新的dex对象到当前APPClassLoader对象(也就是BaseDexClassLoader)的pathList里面的dexElements。要添加就要先创建,我们先使用DexClassLoader先加载插件,然后在生成插件的dexElements,最后再添加就好了。

当然整个过程需要使用反射来实现。除此以外,常用的两种方法是使用apk作为插件和使用dex文件作为插件。下面的两个实现都是对程序中的一个方法进行了修改,然后分别打了 dex包和apk包,程序运行起来执行的方法就是插件里面的方法而不是程序本身的方法。

dex插件

对于dex文件作为插件,和之前说的流程完全一致,先将修改了的类进行打包成dex包,在将dex进行加载,插入到dexElements集合的前面即可。而打包流程是先将.java文件编译成.class文件,然后使用SDK工具打包成dex文件并发布到远程服务端,然后APP端请求下载,下载完毕加载即可。

dex打包工具

Android SDK给我们单独提供了dex打包工具《d8》(在Android 构建工具 28.0.1 及更高版本中):如下图所示:

输入字节码可以是 *.class 文件也可以是JAR、APK 或 ZIP 文件的任意组合。还可以添加 DEX 文件作为 d8 的输入,以将这些文件合并到 DEX 输出中。如下所示,cmd进入要d8文件所在目录,执行要打包命令:

 d8 E:\asworkspace\MyAndServer\app\build\intermediates\classes\debug\*\*.class

到了这一步,我们已经打包好了dex文件,下面看一下具体的实现:

//在Application中进行替换
public class MApplication extends Application {@Overridepublic void onCreate() {super.onCreate();//dex作为插件进行加载dexPlugin();}.../*** dex作为插件加载*/private void dexPlugin(){//插件包文件File file = new File("/sdcard/FixDexTest.dex");if (!file.exists()) {Log.i("MApplication", "插件包不在");return;}try {//获取到 BaseDexClassLoader 的  pathList字段// private final DexPathList pathList;Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");//破坏封装,设置为可以调用pathListField.setAccessible(true);//拿到当前ClassLoader的pathList对象Object pathListObj = pathListField.get(getClassLoader());//获取当前ClassLoader的pathList对象的字节码文件(DexPathList )Class<?> dexPathListClass = pathListObj.getClass();//拿到DexPathList 的 dexElements字段// private final Element[] dexElements;Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");//破坏封装,设置为可以调用dexElementsField.setAccessible(true);//使用插件创建 ClassLoaderDexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());//拿到插件的DexClassLoader 的 pathList对象Object newPathListObj = pathListField.get(pathClassLoader);//拿到插件的pathList对象的 dexElements变量Object newDexElementsObj = dexElementsField.get(newPathListObj);//拿到当前的pathList对象的 dexElements变量Object dexElementsObj=dexElementsField.get(pathListObj);int oldLength = Array.getLength(dexElementsObj);int newLength = Array.getLength(newDexElementsObj);//创建一个dexElements对象Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);//先添加新的dex添加到dexElementfor (int i = 0; i < newLength; i++) {Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));}//再添加之前的dex添加到dexElementfor (int i = 0; i < oldLength; i++) {Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));}//将组建出来的对象设置给 当前ClassLoader的pathList对象dexElementsField.set(pathListObj, concatDexElementsObject);} catch (Exception e) {e.printStackTrace();}}

apk插件

apk作为插件,就是我们重新打了一个新的apk包作为插件,打包很简单方便,缺点就是文件大。使用apk的话就没必要是将dex插入dexElements里面去,直接将之前的dexElements替换就可以了。下面看一下apk插件的具体实现

apk插件的实现

    // apk作为插件加载private void apkPlugin() {//插件包文件File file = new File("/sdcard/FixDexTest.apk");if (!file.exists()) {Log.i("MApplication", "插件包不在");return;}try {//获取到 BaseDexClassLoader 的  pathList字段// private final DexPathList pathList;Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");//破坏封装,设置为可以调用pathListField.setAccessible(true);//拿到当前ClassLoader的pathList对象Object pathListObj = pathListField.get(getClassLoader());//获取当前ClassLoader的pathList对象的字节码文件(DexPathList )Class<?> dexPathListClass = pathListObj.getClass();//拿到DexPathList 的 dexElements字段// private final Element[] dexElements;Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");//破坏封装,设置为可以调用dexElementsField.setAccessible(true);//使用插件创建 ClassLoaderDexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());//拿到插件的DexClassLoader 的 pathList对象Object newPathListObj = pathListField.get(pathClassLoader);//拿到插件的pathList对象的 dexElements变量Object newDexElementsObj = dexElementsField.get(newPathListObj);//将插件的 dexElements对象设置给 当前ClassLoader的pathList对象dexElementsField.set(pathListObj, newDexElementsObj);} catch (Exception e) {e.printStackTrace();}}

常用第三方热修复组件

常用优秀第三方热修复组件可以分为俩大类

  1. Dex插桩(类替换)
    Tinker:微信
    Qzone:QQ空间
    Robust:美团
  2. 底层替换方案,类似反射
    阿里的AndFix

这俩种不同方案各有优缺点,大家可以更具实际情况选择使用的方式。使用方法可以参考各自的官网和开发社区。

总结

经过对 PathClassLoaderDexClassLoaderBaseDexClassLoaderDexPathList 的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取( Element[]``dexElements )到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于 Element 数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bugclass也是存在的,不过是放在了 Element 数组的最后一个元素中,这样就避免拿到有bugclass

Android热修复实现及原理相关推荐

  1. 热修复系列——Android热修复技术进阶篇

    目录 1 前言 2 热修复技术 2.1 Dexposed 2.2 AndFix 2.3 QZone 2.4 Tinker 2.6 epic 2.7 YAHFA 2.8 FastHook 2.9 美团R ...

  2. Android热修复总结

    阿里的andfix 补丁修复 支持的是相应版本的增量更新,例如从版本1制作了bug1的修复包,然后要在bug1基础上制作到bug2的修复包,可以以bug1的apk跟bug2的apk制作修复包,但是感觉 ...

  3. Android 热修复使用Gradle Plugin1.5改造Nuwa插件

    随着谷歌的Gradle插件版本的不断升级,Gradle插件现在最新的已经到了2.1.0-beta1,对应的依赖为com.android.tools.build:gradle:2.0.0-beta6,而 ...

  4. 【Android 热修复】热修复原理 ( 多 Dex 打包机制 | 多 Dex 支持 | Dex 分包设置 | 开发和产品风格设置 | 源码资源 )

    文章目录 一.Dex 打包设置 1.多 Dex 支持 2.Dex 分包设置 3.开发和产品风格设置 ( 非必须 ) 二.完整 build.gradle 配置 1.build.gradle 配置 2.d ...

  5. 【Android 热修复】热修复原理 ( 合并两个 Element[] dexElements | 自定义 Application 加载 Dex 设置 | 源码资源 )

    文章目录 一.合并两个 Element[] dexElements 二. 完整修复包加载工具类 三. 源码资源 一.合并两个 Element[] dexElements 在 [Android 热修复] ...

  6. 【Android 热修复】热修复原理 ( Dex 文件拷贝后续操作 | 外部存储空间权限申请 | 执行效果验证 | 源码资源 )

    文章目录 一.Dex 文件准备 二.外部存储空间权限申请 1.清单文件申请权限 2.动态申请权限 三.文件拷贝 1.文件拷贝 2.执行效果 四. 源码资源 一.Dex 文件准备 在 [Android ...

  7. 【Android 热修复】热修复原理 ( 修复包 Dex 文件准备 | Dex 优化为 Odex | Dex 文件拷贝 | 源码资源 )

    文章目录 一.修复包 Dex 文件准备 二.Odex 优化 三.Dex 文件拷贝 四. 源码资源 一.修复包 Dex 文件准备 异常代码 : 故意写一个异常代码 , 并执行该代码 , 肯定会崩溃 ; ...

  8. 深入解析阿里Android热修复技术原理

    前言:本文框架 什么是热修复? 热修复框架分类 技术原理及特点 Tinker框架解析 各框架对比图 总结 通过阅读本文,你会对热修复技术有更深的认知,本文会列出各类框架的优缺点以及技术原理,文章末尾简 ...

  9. Android热修复技术原理详解(最新最全版本)

    本文框架 什么是热修复? 热修复框架分类 技术原理及特点 Tinker框架解析 各框架对比图 总结   通过阅读本文,你会对热修复技术有更深的认知,本文会列出各类框架的优缺点以及技术原理,文章末尾简单 ...

最新文章

  1. 一个普通大学生的经历
  2. C#模拟网站登录介绍
  3. 企业中squid+iptables多模块的综合应用案例
  4. jquery事件绑定解绑机制源码分析
  5. android imageview 设置网络图片,ImageView加载网络图片
  6. 容器编排技术 -- Kubernetes kubectl create service 命令详解
  7. python语句块规范_Python基础语法——代码规范判断语句循环语句
  8. QRCode二维码生成方案及其在带LOGO型二维码中的应用(2)
  9. b+树 b-树的区别
  10. Oracle 数据库的连接
  11. 预训练模型的下一步?突破Impossible Triangle
  12. mybatis关于factorybean疑问
  13. 线性规划的大M法和非线性规划的拉格朗日乘子法
  14. PWM占空比控制电机转速
  15. 微信公众号 配网 airkiss配网 wifi配网
  16. 微信小程序获取openid和用户信息
  17. 数字鉴相,关于相位差的提取
  18. Unity转微信小游戏与JS交互
  19. PJSIP编译与使用说明
  20. 使用Processing实现井字棋

热门文章

  1. Ubuntu18.04 ROS melodic 版本的rivz教程
  2. python中通过pip安装套件
  3. python使用osgeo库_MAC下python2.7的GDAL库配置问题
  4. 使用carbon_东华大学《Carbon》多孔碳纳米纤维复合膜,优异电磁波吸收性能!
  5. realme Q5系列核心规格曝光:80W快充加持 同价位绝无仅有
  6. 1月至今 微信共对超十万个确认存在欺诈行为的帐号进行了阶梯式处理
  7. 优酷《追光吧!》正式开播 风度、实力成关键词
  8. 禁止电商平台二选一、遛狗必栓绳!5月起有这些新规定
  9. 罗永浩直播间12小时销售破2亿元 网友:《真还传》年内上映指日可待
  10. 4999元起!华为Mate 40今日开启预售:搭载麒麟9000E