搜索
首页系统教程LINUXAndroid 热修复 Tinker 源码分析

Android 热修复 Tinker 源码分析

Mar 25, 2024 am 09:20 AM
linuxlinux教程红帽linux系统linux命令排列linux认证红帽linuxlinux视频

tinker有个非常大的亮点就是自研发了一套dex diff、patch相关算法。本篇文章主要目的就是分析该算法。当然值得注意的是,分析的前提就是需要对dex文件的格式要有一定的认识,否则的话可能会一脸懵逼态。

所以,本文会先对dex文件格式做一个简单的分析,也会做一些简单的实验,最后进入到dex diff,patch算法部分。

一、Dex文件格式浅析

首先简单了解下Dex文件,大家在反编译的时候,都清楚apk中会包含一个或者多个*.dex文件,该文件中存储了我们编写的代码,一般情况下我们还会通过工具转化为jar,然后通过一些工具反编译查看。

jar文件大家应该都清楚,类似于class文件的压缩包,一般情况下,我们直接解压就可以看到一个个class文件。而dex文件我们无法通过解压获取内部的一个个class文件,说明dex文件拥有自己特定的格式:
dex对Java类文件重新排列,将所有JAVA类文件中的常量池分解,消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池,使得相同的字符串、常量在DEX文件中只出现一次,从而减小了文件的体积。
接下来我们看看dex文件的内部结构到底是什么样子。

分析一个文件的组成,最好自己编写一个最简单的dex文件来分析。

(1)编写代码生成dex

首先我们编写一个类Hello.java:

public class Hello{
    public static void main(String[] args){
        System.out.println("hello dex!");
    }
}

然后进行编译:

javac -source 1.7 -target 1.7 Hello.java

最后通过dx工作将其转化为dex文件:

dx --dex --output=Hello.dex Hello.class

dx路径在Android-sdk/build-tools/版本号/dx下,如果无法识别dx命令,记得将该路径放到path下,或者使用绝对路径。
这样我们就得到了一个非常简单的dex文件。

(2)查看dex文件的内部结构

首先展示一张dex文件的大致的内部结构图:

Android 热修复 Tinker 源码分析

当然,单纯从一张图来说明肯定是远远不够的,因为我们后续要研究diff,patch算法,理论上我们应该要知道更多的细节,甚至要细致到:一个dex文件的每个字节表示的是什么内容。

对于一个类似于二进制的文件,最好的办法肯定不是靠记忆,好在有这么一个软件可以帮助我们的分析:

  • 软件名称:010 Editor

下载完成安装后,打开我们的dex文件,会引导你安装dex文件的解析模板。

最终打开效果图如下:

Android 热修复 Tinker 源码分析

上面部分代表了dex文件的内容(16进制的方式展示),下面部分展示了dex文件的各个区域,你可以通过点击下面部分,来查看其对应的内容区域以及内容。

当然这里也非常建议,阅读一些专门的文章来加深对dex文件的理解:

  • DEX文件格式分析
  • Android逆向之旅—解析编译之后的Dex文件格式

本文也仅会对dex文件做简单的格式分析。

(3)dex文件内部结构简单分析

dex_header
首先我们队dex_header做一个大致的分析,header中包含如下字段:

Android 热修复 Tinker 源码分析

首先我们猜测下header的作用,可以看到起包含了一些校验相关的字段,和整个dex文件大致区块的分布(off都为偏移量)。

这样的好处就是,当虚拟机读取dex文件时,只需要读取出header部分,就可以知道dex文件的大致区块分布了;并且可以检验出该文件格式是否正确、文件是否被篡改等。

  • 能够证明该文件是dex文件
  • checksum和signature主要用于校验文件的完整性
  • file_size为dex文件的大小
  • head_size为头文件的大小
  • endian_tag预设值为12345678,标识默认采用Little-Endian(自行搜索)。

剩下的几乎都是成对出现的size和off,大多代表各区块的包含的特定数据结构的数量和偏移量。例如:string_ids_off为112,指的是偏移量112开始为string_ids区域;string_ids_size为14,代表string_id_item的数量为14个。剩下的都类似就不介绍了。

结合010Editor可以看到各个区域包含的数据结构,以及对应的值,慢慢看就好了。

dex_map_list

除了header还有个比较重要的部分是dex_map_list,首先看个图:

Android 热修复 Tinker 源码分析

首先是map_item_list数量,接下来是每个map_item_list的描述。

map_item_list有什么用呢?

Android 热修复 Tinker 源码分析

可以看到每个map_list_item包含一个枚举类型,一个2字节暂未使用的成员、一个size表明当前类型的个数,offset表明当前类型偏移量。

拿本例来说:

  • 首先是TYPE_HEADER_ITEM类型,包含1个header(size=1),且偏移量为0。
  • 接下来是TYPE_STRING_ID_ITEM,包含14个string_id_item(size=14),且偏移量为112(如果有印象,header的长度为112,紧跟着header)。

剩下的依次类推~~

这样的话,可以看出通过map_list,可以将一个完整的dex文件划分成固定的区域(本例为13),且知道每个区域的开始,以及该区域对应的数据格式的个数。

通过map_list找到各个区域的开始,每个区域都会对应特定的数据结构,通过010 Editor看就好了。

二、分析前的思考

现在我们了解了dex的基本格式,接下来我们考虑下如何做dex diff 和 patch。

先要考虑的是我们有什么:

  1. old dex
  2. new dex

我们想要生成一个patch文件,该文件和old dex 通过patch算法还能生成new dex。

  • 那么我们该如何做呢?

根据上文的分析,我们知道dex文件大致有3个部分(这里3个部分主要用于分析,勿较真):

  1. header
  2. 各个区域
  3. map list

header实际上是可以根据后面的数据确定其内容的,并且是定长112的;各个区域后面说;map list实际上可以做到定位到各个区域开始位置;

我们最终patch + old dex -> new dex;针对上述的3个部分,

  • header我们可以不做处理,因为可以根据其他数据生成;
  • map list这个东西,其实我们主要要的是各个区域的开始(offset)
  • 知道了各个区域的offset后,在我们生成new dex的时候,我们就可以定位各个区域的开始和结束位置,那么只需要往各个区域写数据即可。

那么我们看看针对一个区域的diff,假设有个string区域,主要用于存储字符串:

old dex该区域的字符串有: Hello、World、zhy
new dex该区域的字符串有: Android、World、zhy

可以看出,针对该区域,我们删除了Hello,增加了Android。

那么patch中针对该区域可以如下记录:
“del Hello , add Android”(实际情况需要转化为二进制)。

想想应用中可以直接读取出old dex,即知道:

  • 原来该区域包含:Hello、World、zhy
  • patch中该区域包含:”del Hello , add Android”

那么,可以非常容易的计算出new dex中包含:

Android、World、zhy。

这样我们就完成了一个区域大致的diff和patch算法,其他各个区域的diff和patch和上述类似。

这样来看,是不是觉得这个diff和patch算法也没有那么的复杂,实际上tinker的做法与上述类似,实际情况可能要比上述描述要复杂一些,但是大体上是差不多的。

有了一个大致的算法概念之后,我们就可以去看源码了。

三、Tinker DexDiff源码浅析

这里看代码实际上也是有技巧的,tinker的代码实际上蛮多的,往往你可以会陷在一堆的代码中。我们可以这么考虑,比如diff算法,输入参数为old dex 、new dex,输出为patch file。

那么肯定存在某个类,或者某个方法接受和输出上述参数。实际上该类为DexPatchGenerator:

diff的API使用代码为:

@Test
public void testDiff() throws IOException {
    File oldFile = new File("Hello.dex");
    File newFile = new File("Hello-World.dex");

    File patchFile = new File("patch.dex");
    DexPatchGenerator dexPatchGenerator
            = new DexPatchGenerator(oldFile, newFile);
    dexPatchGenerator.executeAndSaveTo(patchFile);
}

代码在tinker-build的tinker-patch-lib下。

写一个单元测试或者main方法,上述几行代码就是diff算法。

所以查看代码时要有针对性,比如看diff算法,就找到diff算法的入口,不要在gradle plugin中去纠结。

(1)dex file => Dex
public DexPatchGenerator(File oldDexFile, File newDexFile) throws IOException {
    this(new Dex(oldDexFile), new Dex(newDexFile));
}

将我们传入的dex文件转化为了Dex对象。

public Dex(File file) throws IOException {
    // 删除了一堆代码
    InputStream  in = new BufferedInputStream(new FileInputStream(file));
    loadFrom(in, (int) file.length());     
}

private void loadFrom(InputStream in, int initSize) throws IOException {
    byte[] rawData = FileUtils.readStream(in, initSize);
    this.data = ByteBuffer.wrap(rawData);
    this.data.order(ByteOrder.LITTLE_ENDIAN);
    this.tableOfContents.readFrom(this);
}

首先将我们的文件读取为byte[]数组(这里还是蛮耗费内存的),然后由ByteBuffer进行包装,并设置字节顺序为小端(这里说明ByteBuffer还是蛮方便的。然后通过readFrom方法为Dex对象的tableOfContents赋值。

#TableOfContents
public void readFrom(Dex dex) throws IOException {
    readHeader(dex.openSection(header));
    // special case, since mapList.byteCount is available only after
    // computeSizesFromOffsets() was invoked, so here we can't use
    // dex.openSection(mapList) to get dex section. Or
    // an {@code java.nio.BufferUnderflowException} will be thrown.
    readMap(dex.openSection(mapList.off));
    computeSizesFromOffsets();
}

在其内部执行了readHeader和readMap,上文我们大致分析了header和map list相关,实际上就是将这两个区域转化为一定的数据结构,读取然后存储到内存中。

首先看readHeader:

private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException {
    byte[] magic = headerIn.readByteArray(8);
    int apiTarget = DexFormat.magicToApi(magic);

    if (apiTarget != DexFormat.API_NO_EXTENDED_OPCODES) {
        throw new DexException("Unexpected magic: " + Arrays.toString(magic));
    }

    checksum = headerIn.readInt();
    signature = headerIn.readByteArray(20);
    fileSize = headerIn.readInt();
    int headerSize = headerIn.readInt();
    if (headerSize != SizeOf.HEADER_ITEM) {
        throw new DexException("Unexpected header: 0x" + Integer.toHexString(headerSize));
    }
    int endianTag = headerIn.readInt();
    if (endianTag != DexFormat.ENDIAN_TAG) {
        throw new DexException("Unexpected endian tag: 0x" + Integer.toHexString(endianTag));
    }
    linkSize = headerIn.readInt();
    linkOff = headerIn.readInt();
    mapList.off = headerIn.readInt();
    if (mapList.off == 0) {
        throw new DexException("Cannot merge dex files that do not contain a map");
    }
    stringIds.size = headerIn.readInt();
    stringIds.off = headerIn.readInt();
    typeIds.size = headerIn.readInt();
    typeIds.off = headerIn.readInt();
    protoIds.size = headerIn.readInt();
    protoIds.off = headerIn.readInt();
    fieldIds.size = headerIn.readInt();
    fieldIds.off = headerIn.readInt();
    methodIds.size = headerIn.readInt();
    methodIds.off = headerIn.readInt();
    classDefs.size = headerIn.readInt();
    classDefs.off = headerIn.readInt();
    dataSize = headerIn.readInt();
    dataOff = headerIn.readInt();
}

如果你现在打开着010 Editor,或者看一眼最前面的图,实际上就是将header中所有的字段定义出来,读取响应的字节并赋值。

接下来看readMap:

private void readMap(Dex.Section in) throws IOException {
    int mapSize = in.readInt();
    Section previous = null;
    for (int i = 0; i 
<p>这里注意,在读取header的时候,实际上已经读取除了map list区域的offset,并存储在mapList.off中。所以map list中实际上是从这个位置开始的。首先读取的就是map_list_item的个数,接下来读取的就是每个map_list_item对应的实际数据。</p>
<p>可以看到依次读取:type,unused,size,offset,如果你还有印象前面我们描述了map_list_item是与此对应的,对应的数据结构为TableContents.Section对象。</p>
<p>computeSizesFromOffsets()主要为section的byteCount(占据了多个字节)参数赋值。</p>
<p>到这里就完成了dex file 到 Dex对象的初始化。</p>
<p>有了两个Dex对象之后,就需要去做diff操作了。</p>
<div style="margin-top: 2em; margin-bottom: 1em;"><span style="color: #1e1e1e; letter-spacing: 2px; border-left: #FF3030 3px solid; border-right: #FF3030 3px solid; padding-left: 8px; padding-right: 8px; font-size: 12pt;"><strong>(2)dex diff</strong></span></div>
<p>继续回到源码:</p>
<pre class="brush:php;toolbar:false">public DexPatchGenerator(File oldDexFile, InputStream newDexStream) throws IOException {
    this(new Dex(oldDexFile), new Dex(newDexStream));
}

直接到两个Dex对象的构造函数:

public DexPatchGenerator(Dex oldDex, Dex newDex) {
    this.oldDex = oldDex;
    this.newDex = newDex;

    SparseIndexMap oldToNewIndexMap = new SparseIndexMap();
    SparseIndexMap oldToPatchedIndexMap = new SparseIndexMap();
    SparseIndexMap newToPatchedIndexMap = new SparseIndexMap();
    SparseIndexMap selfIndexMapForSkip = new SparseIndexMap();

    additionalRemovingClassPatternSet = new HashSet();

    this.stringDataSectionDiffAlg = new StringDataSectionDiffAlgorithm(
            oldDex, newDex,
            oldToNewIndexMap,
            oldToPatchedIndexMap,
            newToPatchedIndexMap,
            selfIndexMapForSkip
    );
    this.typeIdSectionDiffAlg = ...
    this.protoIdSectionDiffAlg = ...
    this.fieldIdSectionDiffAlg = ...
    this.methodIdSectionDiffAlg = ...
    this.classDefSectionDiffAlg = ...
    this.typeListSectionDiffAlg = ...
    this.annotationSetRefListSectionDiffAlg = ... 
    this.annotationSetSectionDiffAlg = ...
    this.classDataSectionDiffAlg = ... 
    this.codeSectionDiffAlg = ...
    this.debugInfoSectionDiffAlg = ...
    this.annotationSectionDiffAlg = ...
    this.encodedArraySectionDiffAlg = ...
    this.annotationsDirectorySectionDiffAlg = ...
}

看到其首先为oldDex,newDex赋值,然后依次初始化了15个算法,每个算法代表每个区域,算法的目的就像我们之前描述的那样,要知道“删除了哪些,新增了哪些”;

我们继续看代码:

dexPatchGenerator.executeAndSaveTo(patchFile);

有了dexPatchGenerator对象后,直接指向了executeAndSaveTo方法。

public void executeAndSaveTo(File file) throws IOException {
    OutputStream os = null;
    try {
        os = new BufferedOutputStream(new FileOutputStream(file));
        executeAndSaveTo(os);
    } finally {
        if (os != null) {
            try {
                os.close();
            } catch (Exception e) {
                // ignored.
            }
        }
    }
}

到executeAndSaveTo方法:

public void executeAndSaveTo(OutputStream out) throws IOException {
    int patchedheaderSize = SizeOf.HEADER_ITEM;
    int patchedStringIdsSize = newDex.getTableOfContents().stringIds.size * SizeOf.STRING_ID_ITEM;
    int patchedTypeIdsSize = newDex.getTableOfContents().typeIds.size * SizeOf.TYPE_ID_ITEM;

    int patchedProtoIdsSize = newDex.getTableOfContents().protoIds.size * SizeOf.PROTO_ID_ITEM;

    int patchedFieldIdsSize = newDex.getTableOfContents().fieldIds.size * SizeOf.MEMBER_ID_ITEM;
    int patchedMethodIdsSize = newDex.getTableOfContents().methodIds.size * SizeOf.MEMBER_ID_ITEM;
    int patchedClassDefsSize = newDex.getTableOfContents().classDefs.size * SizeOf.CLASS_DEF_ITEM;

    int patchedIdSectionSize =
            patchedStringIdsSize
                    + patchedTypeIdsSize
                    + patchedProtoIdsSize
                    + patchedFieldIdsSize
                    + patchedMethodIdsSize
                    + patchedClassDefsSize;

    this.patchedHeaderOffset = 0;

    this.patchedStringIdsOffset = patchedHeaderOffset + patchedheaderSize;

    this.stringDataSectionDiffAlg.execute();
    this.patchedStringDataItemsOffset = patchedheaderSize + patchedIdSectionSize;

    this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);

    // 省略了其余14个算法的一堆代码
    this.patchedDexSize
            = this.patchedMapListOffset
            + patchedMapListSize;
    writeResultToStream(out);
}

因为涉及到15个算法,所以这里的代码非常长,我们这里只拿其中一个算法来说明。

每个算法都会执行execute和simulatePatchOperation方法:

首先看execute:

public void execute() {
    this.patchOperationList.clear();

    // 1. 拿到oldDex和newDex的itemList
    this.adjustedOldIndexedItemsWithOrigOrder = collectSectionItems(this.oldDex, true);
    this.oldItemCount = this.adjustedOldIndexedItemsWithOrigOrder.length;

    AbstractMap.SimpleEntry<integer t>[] adjustedOldIndexedItems = new AbstractMap.SimpleEntry[this.oldItemCount];
    System.arraycopy(this.adjustedOldIndexedItemsWithOrigOrder, 0, adjustedOldIndexedItems, 0, this.oldItemCount);
    Arrays.sort(adjustedOldIndexedItems, this.comparatorForItemDiff);

    AbstractMap.SimpleEntry<integer t>[] adjustedNewIndexedItems = collectSectionItems(this.newDex, false);
    this.newItemCount = adjustedNewIndexedItems.length;
    Arrays.sort(adjustedNewIndexedItems, this.comparatorForItemDiff);

    int oldCursor = 0;
    int newCursor = 0;
    // 2.遍历,对比,收集patch操作
    while (oldCursor = this.oldItemCount) {
            // rest item are all newItem.
            while (newCursor = newItemCount) {
            // rest item are all oldItem.
            while (oldCursor  oldIndexedItem = adjustedOldIndexedItems[oldCursor];
            AbstractMap.SimpleEntry<integer t> newIndexedItem = adjustedNewIndexedItems[newCursor];
            int cmpRes = oldIndexedItem.getValue().compareTo(newIndexedItem.getValue());
            if (cmpRes (PatchOperation.OP_DEL, deletedIndex));
                markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
                ++oldCursor;
            } else if (cmpRes > 0) {
                this.patchOperationList.add(new PatchOperation(PatchOperation.OP_ADD,
                        newIndexedItem.getKey(), newIndexedItem.getValue()));
                ++newCursor;
            } else {
                int oldIndex = oldIndexedItem.getKey();
                int newIndex = newIndexedItem.getKey();
                int oldOffset = getItemOffsetOrIndex(oldIndexedItem.getKey(), oldIndexedItem.getValue());
                int newOffset = getItemOffsetOrIndex(newIndexedItem.getKey(), newIndexedItem.getValue());

                if (oldIndex != newIndex) {
                    this.oldIndexToNewIndexMap.put(oldIndex, newIndex);
                }

                if (oldOffset != newOffset) {
                    this.oldOffsetToNewOffsetMap.put(oldOffset, newOffset);
                }

                ++oldCursor;
                ++newCursor;
            }
        }
    }

    // 未完
}</integer></integer></integer>

可以看到首先读取oldDex和newDex对应区域的数据并排序,分别adjustedOldIndexedItems和adjustedNewIndexedItems。

接下来就开始遍历了,直接看else部分:

分别根据当前的cursor,获取oldItem和newItem,对其value对对比:

  • 如果
  • 如果>0,则认为该newItem是新增的,记录为PatchOperation.OP_ADD,并记录该newItem index和value到PatchOperation对象,加入到patchOperationList中。
  • 如果=0,不会生成PatchOperation。

经过上述,我们得到了一个patchOperationList对象。

继续下半部分代码:

public void execute() {
    // 接上...

    // 根据index排序,如果index一样,则先DEL后ADD
    Collections.sort(this.patchOperationList, comparatorForPatchOperationOpt);

    Iterator<patchoperation>> patchOperationIt = this.patchOperationList.iterator();
    PatchOperation<t> prevPatchOperation = null;
    while (patchOperationIt.hasNext()) {
        PatchOperation<t> patchOperation = patchOperationIt.next();
        if (prevPatchOperation != null
                && prevPatchOperation.op == PatchOperation.OP_DEL
                && patchOperation.op == PatchOperation.OP_ADD
                ) {
            if (prevPatchOperation.index == patchOperation.index) {
                prevPatchOperation.op = PatchOperation.OP_REPLACE;
                prevPatchOperation.newItem = patchOperation.newItem;
                patchOperationIt.remove();
                prevPatchOperation = null;
            } else {
                prevPatchOperation = patchOperation;
            }
        } else {
            prevPatchOperation = patchOperation;
        }
    }

    // Finally we record some information for the final calculations.
    patchOperationIt = this.patchOperationList.iterator();
    while (patchOperationIt.hasNext()) {
        PatchOperation<t> patchOperation = patchOperationIt.next();
        switch (patchOperation.op) {
            case PatchOperation.OP_DEL: {
                indexToDelOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
            case PatchOperation.OP_ADD: {
                indexToAddOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
            case PatchOperation.OP_REPLACE: {
                indexToReplaceOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
        }
    }
}</t></t></t></patchoperation>
  1. 首先对patchOperationList按照index排序,如果index一致则先DEL、后ADD。
  2. 接下来一个对所有的operation的迭代,主要将index一致的,且连续的DEL、ADD转化为REPLACE操作。
  3. 最后将patchOperationList转化为3个Map,分别为:indexToDelOperationMap,indexToAddOperationMap,indexToReplaceOperationMap。

ok,经历完成execute之后,我们主要的产物就是3个Map,分别记录了:oldDex中哪些index需要删除;newDex中新增了哪些item;哪些item需要替换为新item。

刚才说了每个算法除了execute()还有个simulatePatchOperation()

this.stringDataSectionDiffAlg
    .simulatePatchOperation(this.patchedStringDataItemsOffset);

传入的偏移量为data区域的偏移量。

public void simulatePatchOperation(int baseOffset) {
    int oldIndex = 0;
    int patchedIndex = 0;
    int patchedOffset = baseOffset;
    while (oldIndex 
<p>遍历oldIndex与newIndex,分别在indexToAddOperationMap,indexToReplaceOperationMap,indexToDelOperationMap中查找。</p>
<p>这里关注一点最终的一个产物是this.patchedSectionSize,由patchedOffset-baseOffset所得。<br>
这里有几种情况会造成patchedOffset+=itemSize:</p>
<ol>
<li>indexToAddOperationMap中包含patchIndex</li>
<li>indexToReplaceOperationMap包含patchIndex</li>
<li>不在indexToDelOperationMap与indexToReplaceOperationMap中的oldDex.</li>
</ol>
<p>其实很好理解,这个patchedSectionSize其实对应newDex的这个区域的size。所以,包含需要ADD的Item,会被替代的Item,以及OLD ITEMS中没有被删除和替代的Item。这三者相加即为newDex的itemList。</p>
<p>到这里,一个算法就执行完毕了。</p>
<p>经过这样的一个算法,我们得到了PatchOperationList和对应区域sectionSize。那么执行完成所有的算法,应该会得到针对每个算法的PatchOperationList,和每个区域的sectionSize;每个区域的sectionSize实际上换算得到每个区域的offset。</p>
<p>每个区域的算法,execute和simulatePatchOperation代码都是复用的,所以其他的都只有细微的变化,可以自己看了。</p>
<p>接下来看执行完成所有的算法后的writeResultToStream方法。</p>
<div style="margin-top: 2em; margin-bottom: 1em;"><span style="color: #1e1e1e; letter-spacing: 2px; border-left: #FF3030 3px solid; border-right: #FF3030 3px solid; padding-left: 8px; padding-right: 8px; font-size: 12pt;"><strong>(3) 生成patch文件</strong></span></div>
<pre class="brush:php;toolbar:false">private void writeResultToStream(OutputStream os) throws IOException {
    DexDataBuffer buffer = new DexDataBuffer();
    buffer.write(DexPatchFile.MAGIC); // DEXDIFF
    buffer.writeShort(DexPatchFile.CURRENT_VERSION); /0x0002
    buffer.writeInt(this.patchedDexSize);
    // we will return here to write firstChunkOffset later.
    int posOfFirstChunkOffsetField = buffer.position();
    buffer.writeInt(0);
    buffer.writeInt(this.patchedStringIdsOffset);
    buffer.writeInt(this.patchedTypeIdsOffset);
    buffer.writeInt(this.patchedProtoIdsOffset);
    buffer.writeInt(this.patchedFieldIdsOffset);
    buffer.writeInt(this.patchedMethodIdsOffset);
    buffer.writeInt(this.patchedClassDefsOffset);
    buffer.writeInt(this.patchedMapListOffset);
    buffer.writeInt(this.patchedTypeListsOffset);
    buffer.writeInt(this.patchedAnnotationSetRefListItemsOffset);
    buffer.writeInt(this.patchedAnnotationSetItemsOffset);
    buffer.writeInt(this.patchedClassDataItemsOffset);
    buffer.writeInt(this.patchedCodeItemsOffset);
    buffer.writeInt(this.patchedStringDataItemsOffset);
    buffer.writeInt(this.patchedDebugInfoItemsOffset);
    buffer.writeInt(this.patchedAnnotationItemsOffset);
    buffer.writeInt(this.patchedEncodedArrayItemsOffset);
    buffer.writeInt(this.patchedAnnotationsDirectoryItemsOffset);
    buffer.write(this.oldDex.computeSignature(false));
    int firstChunkOffset = buffer.position();
    buffer.position(posOfFirstChunkOffsetField);
    buffer.writeInt(firstChunkOffset);
    buffer.position(firstChunkOffset);

    writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList());
    // 省略其他14个writePatch...

    byte[] bufferData = buffer.array();
    os.write(bufferData);
    os.flush();
}
  • 首先写了MAGIC,CURRENT_VERSION主要用于检查该文件为合法的tinker patch 文件。
  • 然后写入patchedDexSize
  • 第四位写入的是数据区的offset,可以看到先使用0站位,等所有的map list相关的offset书写结束,写入当前的位置。
  • 接下来写入所有的跟maplist各个区域相关的offset(这里各个区域的排序不重要,读写一致即可)
  • 然后执行每个算法写入对应区域的信息
  • 最后生成patch文件

我们依旧只看stringDataSectionDiffAlg这个算法。

private <t extends comparable>> void writePatchOperations(
        DexDataBuffer buffer, List<patchoperation>> patchOperationList
) {
    List<integer> delOpIndexList = new ArrayList(patchOperationList.size());
    List<integer> addOpIndexList = new ArrayList(patchOperationList.size());
    List<integer> replaceOpIndexList = new ArrayList(patchOperationList.size());

    List<t> newItemList = new ArrayList(patchOperationList.size());

    for (PatchOperation<t> patchOperation : patchOperationList) {
        switch (patchOperation.op) {
            case PatchOperation.OP_DEL: {
                delOpIndexList.add(patchOperation.index);
                break;
            }
            case PatchOperation.OP_ADD: {
                addOpIndexList.add(patchOperation.index);
                newItemList.add(patchOperation.newItem);
                break;
            }
            case PatchOperation.OP_REPLACE: {
                replaceOpIndexList.add(patchOperation.index);
                newItemList.add(patchOperation.newItem);
                break;
            }
        }
    }

    buffer.writeUleb128(delOpIndexList.size());
    int lastIndex = 0;
    for (Integer index : delOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    buffer.writeUleb128(addOpIndexList.size());
    lastIndex = 0;
    for (Integer index : addOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    buffer.writeUleb128(replaceOpIndexList.size());
    lastIndex = 0;
    for (Integer index : replaceOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    for (T newItem : newItemList) {
        if (newItem instanceof StringData) {
            buffer.writeStringData((StringData) newItem);
        } 
        // else 其他类型,write其他类型Data

    }
}</t></t></integer></integer></integer></patchoperation></t>

首先将我们的patchOperationList转化为3个OpIndexList,分别对应DEL,ADD,REPLACE,以及将所有的item存入newItemList。

然后依次写入:

  1. del操作的个数,每个del的index
  2. add操作的个数,每个add的index
  3. replace操作的个数,每个需要replace的index
  4. 最后依次写入newItemList.

这里index都做了(这里做了个index – lastIndex操作)

其他的算法也是执行了类似的操作。

最好来看看我们生成的patch是什么样子的:

  1. 首先包含几个字段,证明自己是tinker patch
  2. 包含生成newDex各个区域的offset,即可以将newDex划分了多个区域,定位到起点
  3. 包含newDex各个区域的Item的删除的索引(oldDex),新增的索引和值,替换的索引和值

那么这么看,我们猜测Patch的逻辑时这样的:

  1. 首先根据各个区域的offset,确定各个区域的起点
  2. 读取oldDex各个区域的items,然后根据patch中去除掉oldDex中需要删除的和需要替换的item,再加上新增的item和替换的item即可组成newOld该区域的items。

即,newDex的某个区域的包含:

oldItems - del - replace + addItems + replaceItems

这么看挺清晰的,下面看代码咯~

四、Tinker DexPatch源码浅析
(1)寻找入口

与diff一样,肯定有那么一个类或者方法,接受old dex File 和 patch File,最后生成new Dex。不要陷在一堆安全校验,apk解压的代码中。

这个类叫做DexPatchApplier,在tinker-commons中。

patch的相关代码如下:

@Test
public void testPatch() throws IOException {
    File oldFile = new File("Hello.dex");
    File patchFile = new File("patch.dex");

    File newFile = new File("new.dex");

    DexPatchApplier dexPatchGenerator
            = new DexPatchApplier(oldFile, patchFile);
    dexPatchGenerator.executeAndSaveTo(newFile);
}

可以看到和diff代码类似,下面看代码去。

(2)源码分析
public DexPatchApplier(File oldDexIn, File patchFileIn) throws IOException {
    this(new Dex(oldDexIn), new DexPatchFile(patchFileIn));
}

oldDex会转化为Dex对象,这个上面分析过,主要就是readHeader和readMap.注意我们的patchFile是转为一个DexPatchFile对象。

public DexPatchFile(File file) throws IOException {
    this.buffer = new DexDataBuffer(ByteBuffer.wrap(FileUtils.readFile(file)));
    init();
}

首先将patch file读取为byte[],然后调用init

private void init() {
    byte[] magic = this.buffer.readByteArray(MAGIC.length);
    if (CompareUtils.uArrCompare(magic, MAGIC) != 0) {
        throw new IllegalStateException("bad dex patch file magic: " + Arrays.toString(magic));
    }

    this.version = this.buffer.readShort();
    if (CompareUtils.uCompare(this.version, CURRENT_VERSION) != 0) {
        throw new IllegalStateException("bad dex patch file version: " + this.version + ", expected: " + CURRENT_VERSION);
    }

    this.patchedDexSize = this.buffer.readInt();
    this.firstChunkOffset = this.buffer.readInt();
    this.patchedStringIdSectionOffset = this.buffer.readInt();
    this.patchedTypeIdSectionOffset = this.buffer.readInt();
    this.patchedProtoIdSectionOffset = this.buffer.readInt();
    this.patchedFieldIdSectionOffset = this.buffer.readInt();
    this.patchedMethodIdSectionOffset = this.buffer.readInt();
    this.patchedClassDefSectionOffset = this.buffer.readInt();
    this.patchedMapListSectionOffset = this.buffer.readInt();
    this.patchedTypeListSectionOffset = this.buffer.readInt();
    this.patchedAnnotationSetRefListSectionOffset = this.buffer.readInt();
    this.patchedAnnotationSetSectionOffset = this.buffer.readInt();
    this.patchedClassDataSectionOffset = this.buffer.readInt();
    this.patchedCodeSectionOffset = this.buffer.readInt();
    this.patchedStringDataSectionOffset = this.buffer.readInt();
    this.patchedDebugInfoSectionOffset = this.buffer.readInt();
    this.patchedAnnotationSectionOffset = this.buffer.readInt();
    this.patchedEncodedArraySectionOffset = this.buffer.readInt();
    this.patchedAnnotationsDirectorySectionOffset = this.buffer.readInt();
    this.oldDexSignature = this.buffer.readByteArray(SizeOf.SIGNATURE);

    this.buffer.position(firstChunkOffset);
}

还记得我们写patch的操作么,先写了MAGIC和Version用于校验该文件是一个patch file;接下来为patchedDexSize和各种offset进行赋值;最后定位到数据区(firstChunkOffset),还记得写的时候,该字段在第四个位置。

定位到该位置后,后面读取的就是数据了,数据存的时候按照如下格式存储的:

  1. del操作的个数,每个del的index
  2. add操作的个数,每个add的index
  3. replace操作的个数,每个需要replace的index
  4. 最后依次写入newItemList.

简单回忆下,我们继续源码分析。

public DexPatchApplier(File oldDexIn, File patchFileIn) throws IOException {
    this(new Dex(oldDexIn), new DexPatchFile(patchFileIn));
}

public DexPatchApplier(
        Dex oldDexIn,
        DexPatchFile patchFileIn) {
    this.oldDex = oldDexIn;
    this.patchFile = patchFileIn;
    this.patchedDex = new Dex(patchFileIn.getPatchedDexSize());
    this.oldToPatchedIndexMap = new SparseIndexMap();
}

除了oldDex,patchFile,还初始化了一个patchedDex作为我们最终输出Dex对象。

构造完成后,直接执行了executeAndSaveTo方法。

public void executeAndSaveTo(File file) throws IOException {
    OutputStream os = null;
    try {
        os = new BufferedOutputStream(new FileOutputStream(file));
        executeAndSaveTo(os);
    } finally {
        if (os != null) {
            try {
                os.close();
            } catch (Exception e) {
                // ignored.
            }
        }
    }
}

直接到executeAndSaveTo(os),该方法代码比较长,我们分3段讲解:

public void executeAndSaveTo(OutputStream out) throws IOException {

    TableOfContents patchedToc = this.patchedDex.getTableOfContents();

    patchedToc.header.off = 0;
    patchedToc.header.size = 1;
    patchedToc.mapList.size = 1;

    patchedToc.stringIds.off
            = this.patchFile.getPatchedStringIdSectionOffset();
    patchedToc.typeIds.off
            = this.patchFile.getPatchedTypeIdSectionOffset();
    patchedToc.typeLists.off
            = this.patchFile.getPatchedTypeListSectionOffset();
    patchedToc.protoIds.off
            = this.patchFile.getPatchedProtoIdSectionOffset();
    patchedToc.fieldIds.off
            = this.patchFile.getPatchedFieldIdSectionOffset();
    patchedToc.methodIds.off
            = this.patchFile.getPatchedMethodIdSectionOffset();
    patchedToc.classDefs.off
            = this.patchFile.getPatchedClassDefSectionOffset();
    patchedToc.mapList.off
            = this.patchFile.getPatchedMapListSectionOffset();
    patchedToc.stringDatas.off
            = this.patchFile.getPatchedStringDataSectionOffset();
    patchedToc.annotations.off
            = this.patchFile.getPatchedAnnotationSectionOffset();
    patchedToc.annotationSets.off
            = this.patchFile.getPatchedAnnotationSetSectionOffset();
    patchedToc.annotationSetRefLists.off
            = this.patchFile.getPatchedAnnotationSetRefListSectionOffset();
    patchedToc.annotationsDirectories.off
            = this.patchFile.getPatchedAnnotationsDirectorySectionOffset();
    patchedToc.encodedArrays.off
            = this.patchFile.getPatchedEncodedArraySectionOffset();
    patchedToc.debugInfos.off
            = this.patchFile.getPatchedDebugInfoSectionOffset();
    patchedToc.codes.off
            = this.patchFile.getPatchedCodeSectionOffset();
    patchedToc.classDatas.off
            = this.patchFile.getPatchedClassDataSectionOffset();
    patchedToc.fileSize
            = this.patchFile.getPatchedDexSize();

    Arrays.sort(patchedToc.sections);

    patchedToc.computeSizesFromOffsets();

    // 未完待续...

}

这里实际上,就是读取patchFile中记录的值给patchedDex的TableOfContent中各种Section(大致对应map list中各个map_list_item)赋值。

接下来排序呢,设置byteCount等字段信息。

继续:

public void executeAndSaveTo(OutputStream out) throws IOException {

    // 省略第一部分代码

    // Secondly, run patch algorithms according to sections' dependencies.
    this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.protoIdSectionPatchAlg = new ProtoIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.fieldIdSectionPatchAlg = new FieldIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.methodIdSectionPatchAlg = new MethodIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.classDefSectionPatchAlg = new ClassDefSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.typeListSectionPatchAlg = new TypeListSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationSetRefListSectionPatchAlg = new AnnotationSetRefListSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationSetSectionPatchAlg = new AnnotationSetSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.classDataSectionPatchAlg = new ClassDataSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.codeSectionPatchAlg = new CodeSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.debugInfoSectionPatchAlg = new DebugInfoItemSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationSectionPatchAlg = new AnnotationSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.encodedArraySectionPatchAlg = new StaticValueSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationsDirectorySectionPatchAlg = new AnnotationsDirectorySectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );

    this.stringDataSectionPatchAlg.execute();
    this.typeIdSectionPatchAlg.execute();
    this.typeListSectionPatchAlg.execute();
    this.protoIdSectionPatchAlg.execute();
    this.fieldIdSectionPatchAlg.execute();
    this.methodIdSectionPatchAlg.execute();
    this.annotationSectionPatchAlg.execute();
    this.annotationSetSectionPatchAlg.execute();
    this.annotationSetRefListSectionPatchAlg.execute();
    this.annotationsDirectorySectionPatchAlg.execute();
    this.debugInfoSectionPatchAlg.execute();
    this.codeSectionPatchAlg.execute();
    this.classDataSectionPatchAlg.execute();
    this.encodedArraySectionPatchAlg.execute();
    this.classDefSectionPatchAlg.execute();

    //未完待续...

}

这一部分很明显初始化了一堆算法,然后分别去执行。我们依然是拿stringDataSectionPatchAlg来分析。

public void execute() {
    final int deletedItemCount = patchFile.getBuffer().readUleb128();
    final int[] deletedIndices = readDeltaIndiciesOrOffsets(deletedItemCount);

    final int addedItemCount = patchFile.getBuffer().readUleb128();
    final int[] addedIndices = readDeltaIndiciesOrOffsets(addedItemCount);

    final int replacedItemCount = patchFile.getBuffer().readUleb128();
    final int[] replacedIndices = readDeltaIndiciesOrOffsets(replacedItemCount);

    final TableOfContents.Section tocSec = getTocSection(this.oldDex);
    Dex.Section oldSection = null;

    int oldItemCount = 0;
    if (tocSec.exists()) {
        oldSection = this.oldDex.openSection(tocSec);
        oldItemCount = tocSec.size;
    }

    // Now rest data are added and replaced items arranged in the order of
    // added indices and replaced indices.
    doFullPatch(
            oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices
    );
}

再贴一下我们写入时的规则:

  1. del操作的个数,每个del的index
  2. add操作的个数,每个add的index
  3. replace操作的个数,每个需要replace的index
  4. 最后依次写入newItemList.

看代码,读取顺序如下:

  1. del的数量,del的所有的index存储在一个int[]中;
  2. add的数量,add的所有的index存储在一个int[]中;
  3. replace的数量,replace的所有的index存储在一个int[]中;

是不是和写入时一致。

继续,接下来获取了oldDex中oldItems和oldItemCount。

那么现在有了:

  1. del count and indices
  2. add count add indices
  3. replace count and indices
  4. oldItems and oldItemCount

拿着我们拥有的,继续执行doFullPatch

private void doFullPatch(
        Dex.Section oldSection,
        int oldItemCount,
        int[] deletedIndices,
        int[] addedIndices,
        int[] replacedIndices) {
    int deletedItemCount = deletedIndices.length;
    int addedItemCount = addedIndices.length;
    int replacedItemCount = replacedIndices.length;
    int newItemCount = oldItemCount + addedItemCount - deletedItemCount;

    int deletedItemCounter = 0;
    int addActionCursor = 0;
    int replaceActionCursor = 0;

    int oldIndex = 0;
    int patchedIndex = 0;

    while (oldIndex = 0) {
            T skippedOldItem = nextItem(oldSection); // skip old item.

            ++oldIndex;
            ++deletedItemCounter;
        } else
        if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {
            T skippedOldItem = nextItem(oldSection); // skip old item.

            ++oldIndex;
        } else
        if (oldIndex 
<p>先整体上看一下,这里的目的就是往patchedDex的stringData区写数据,写的数据理论上应该是:</p>
<ol>
<li>新增的数据</li>
<li>替代的数据</li>
<li>oldDex中出去新增和被替代的数据</li>
</ol>
<p>当然他们需要顺序写入。</p>
<p>所以看代码,首先计算出newItemCount=oldItemCount + addCount - delCount,然后开始遍历,遍历条件为0~oldItemCount或0~newItemCount。</p>
<p>我们期望的是,在patchIndex从0~newItemCount之间都会写入对应的Item。</p>
<p>Item写入通过代码我们可以看到:</p>
<ol>
<li>首先判断该patchIndex是否包含在addIndices中,如果包含则写入;</li>
<li>再者判断是否在repalceIndices中,如果包含则写入;</li>
<li>然后判断如果发现oldIndex被delete或者replace,直接跳过;</li>
<li>那么最后一个index指的就是,oldIndex为非delete和replace的,也就是和newDex中items相同的部分。</li>
</ol>
<p>上述1.2.4三个部分即可组成完整的newDex的该区域。</p>
<p>这样的话就完成了stringData区域的patch算法。</p>
<p>其他剩下的14个算法的execute代码是相同的(父类),执行的操作类似,都会完成各个部分的patch算法。</p>
<p>当所有的区域都完成恢复后,那么剩下的就是header和mapList了,所以回到所有算法执行完成的地方:</p>
<pre class="brush:php;toolbar:false">public void executeAndSaveTo(OutputStream out) throws IOException {

    //1.省略了offset的各种赋值
    //2.省略了各个部分的patch算法

    // Thirdly, write header, mapList. Calculate and write patched dex's sign and checksum.
    Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);
    patchedToc.writeHeader(headerOut);

    Dex.Section mapListOut = this.patchedDex.openSection(patchedToc.mapList.off);
    patchedToc.writeMap(mapListOut);

    this.patchedDex.writeHashes();

    // Finally, write patched dex to file.
    this.patchedDex.writeTo(out);
}

定位到header区域,写header相关数据;定位到map list区域,编写map list相关数据。两者都完成的时候,需要编写header中比较特殊的两个字段:签名和checkSum,因为这两个字段是依赖map list的,所以必须在编写map list后。

这样就完成了完整的dex的恢复,最后将内存中的所有数据写到文件中。

五、案例简单分析
(1)dex准备

刚才我们有个Hello.dex,我们再编写一个类:

public class World{
    public static void main(String[] args){
        System.out.println("nani World");
    }
}

然后将这个类编译以及打成dx文件。

javac -source 1.7 -target 1.7 World.java
dx --dex --output=World.dex World.class

这样我们就准备好了两个dex,Hello.dex和World.dex.

(2) diff

使用010 Editor分别打开两个dex,我们主要关注string_id_item;

Android 热修复 Tinker 源码分析

两边分别13个字符串,按照我们上面介绍的diff算法,我们可以得到以下操作:

两边的字符串分别开始遍历对比:

  • 如果
  • 如果>0,则认为该newItem是新增的,记录为PatchOperation.OP_ADD,并记录该newItem index和value到PatchOperation对象,加入到patchOperationList中。
  • 如果=0,不会生成PatchOperation。
del 1
add 1 LWorld; 
del 2
add 8 World.java
del 10
add 11 naniWorld

然后是根据索引排序,没有变化;

接下来遍历所有的操作,将index一致且DEL和ADD相邻的操作替换为replace

replace 1 LWorld
del 2
add 8 World.java
del 10
add 11 naniWorld

最终在write时,会做一次遍历,将操作按DEL,ADD,REPLACE进行分类,并且将出现的item放置到newItemList中。

del ops:
    del 2
    del 10
add ops:
    add 8
    add 11
replace ops:
    replace 1

newItemList变为:

LWorld //replace 1 
World.java //add 8 
naniWorld //add 11

然后写入,那么写入的顺序应该是:

2 //del size
2 
8 // index - lastIndex
2 // add size
8
3 // index - lastIndex
1 //replace size
1
LWorld
World.java
naniWorld

这里我们直接在DexPatchGenerator的writeResultToStream的相关位置打上日志:

buffer.writeUleb128(delOpIndexList.size());
System.out.println("del size = " + delOpIndexList.size());
int lastIndex = 0;
for (Integer index : delOpIndexList) {
    buffer.writeSleb128(index - lastIndex);
    System.out.println("del index = " + (index - lastIndex));
    lastIndex = index;
}
buffer.writeUleb128(addOpIndexList.size());
System.out.println("add size = " + addOpIndexList.size());
lastIndex = 0;
for (Integer index : addOpIndexList) {
    buffer.writeSleb128(index - lastIndex);
    System.out.println("add index = " + (index - lastIndex));
    lastIndex = index;
}
buffer.writeUleb128(replaceOpIndexList.size());
System.out.println("replace size = " + addOpIndexList.size());
lastIndex = 0;
for (Integer index : replaceOpIndexList) {
    buffer.writeSleb128(index - lastIndex);
    System.out.println("replace index = " + (index - lastIndex));

    lastIndex = index;
}

for (T newItem : newItemList) {
    if (newItem instanceof StringData) {
        buffer.writeStringData((StringData) newItem);
        System.out.println("stringdata  = " + ((StringData) newItem).value);
      }
}

可以看到输出为:

del size = 2
del index = 2
del index = 8
add size = 2
add index = 8
add index = 3
replace size = 2
replace index = 1
stringdata  = LWorld;
stringdata  = World.java
stringdata  = nani World

与我们上述分析结果一致 ~~

那么其他区域可以用类似的方式去验证,patch的话也差不多,就不赘述了。

以上是Android 热修复 Tinker 源码分析的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:Linux就该这么学。如有侵权,请联系admin@php.cn删除
Linux和Windows处理设备驱动程序的区别有什么区别?Linux和Windows处理设备驱动程序的区别有什么区别?Apr 25, 2025 am 12:13 AM

Linux和Windows在处理设备驱动程序上的差异主要体现在驱动管理的灵活性和开发环境上。1.Linux采用模块化设计,驱动可以动态加载和卸载,开发者需深入理解内核机制。2.Windows依赖微软生态,驱动需通过WDK开发并签名认证,开发相对复杂但保证了系统的稳定性和安全性。

比较和对比Linux和Windows的安全模型。比较和对比Linux和Windows的安全模型。Apr 24, 2025 am 12:03 AM

Linux和Windows的安全模型各有优势。Linux提供灵活性和可定制性,通过用户权限、文件系统权限和SELinux/AppArmor实现安全。Windows则注重用户友好性,依赖WindowsDefender、UAC、防火墙和BitLocker保障安全。

Linux和Windows之间的硬件兼容性有何不同?Linux和Windows之间的硬件兼容性有何不同?Apr 23, 2025 am 12:15 AM

Linux和Windows在硬件兼容性上不同:Windows有广泛的驱动程序支持,Linux依赖社区和厂商。解决Linux兼容性问题可通过手动编译驱动,如克隆RTL8188EU驱动仓库、编译和安装;Windows用户需管理驱动程序以优化性能。

Linux和Windows之间虚拟化支持有哪些差异?Linux和Windows之间虚拟化支持有哪些差异?Apr 22, 2025 pm 06:09 PM

Linux和Windows在虚拟化支持上的主要区别在于:1)Linux提供KVM和Xen,性能和灵活性突出,适合高定制环境;2)Windows通过Hyper-V支持虚拟化,界面友好,与Microsoft生态系统紧密集成,适合依赖Microsoft软件的企业。

Linux系统管理员的主要任务是什么?Linux系统管理员的主要任务是什么?Apr 19, 2025 am 12:23 AM

Linux系统管理员的主要任务包括系统监控与性能调优、用户管理、软件包管理、安全管理与备份、故障排查与解决、性能优化与最佳实践。1.使用top、htop等工具监控系统性能,并进行调优。2.通过useradd等命令管理用户账户和权限。3.利用apt、yum管理软件包,确保系统更新和安全。4.配置防火墙、监控日志、进行数据备份以确保系统安全。5.通过日志分析和工具使用进行故障排查和解决。6.优化内核参数和应用配置,遵循最佳实践提升系统性能和稳定性。

很难学习Linux吗?很难学习Linux吗?Apr 18, 2025 am 12:23 AM

学习Linux并不难。1.Linux是一个开源操作系统,基于Unix,广泛应用于服务器、嵌入式系统和个人电脑。2.理解文件系统和权限管理是关键,文件系统是层次化的,权限包括读、写和执行。3.包管理系统如apt和dnf使得软件管理方便。4.进程管理通过ps和top命令实现。5.从基本命令如mkdir、cd、touch和nano开始学习,再尝试高级用法如shell脚本和文本处理。6.常见错误如权限问题可以通过sudo和chmod解决。7.性能优化建议包括使用htop监控资源、清理不必要文件和使用sy

Linux管理员的薪水是多少?Linux管理员的薪水是多少?Apr 17, 2025 am 12:24 AM

Linux管理员的平均年薪在美国为75,000至95,000美元,欧洲为40,000至60,000欧元。提升薪资可以通过:1.持续学习新技术,如云计算和容器技术;2.积累项目经验并建立Portfolio;3.建立职业网络,拓展人脉。

Linux的主要目的是什么?Linux的主要目的是什么?Apr 16, 2025 am 12:19 AM

Linux的主要用途包括:1.服务器操作系统,2.嵌入式系统,3.桌面操作系统,4.开发和测试环境。Linux在这些领域表现出色,提供了稳定性、安全性和高效的开发工具。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

功能强大的PHP集成开发环境

安全考试浏览器

安全考试浏览器

Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。