2025年5月

1 HashMap删除报错

1.1 引言

 @Test
public void testPut(){
    Map map = new HashMap();
    map.put("aa","13456");
    map.put("bb","456789");
    map.put("cc","789456");
    map.forEach((a,b)-> {
        if(a.equals("aa")){
            map.remove("aa");
        }
    });
    map.forEach((key, value) -> {
        System.out.println(key + ":" + value);
    });
}

当在使用 foreachHashMap 进行遍历时,同时进行 remove 赋值操作会有问题,异常 ConcurrentModificationException
只是记得集合类在进行遍历时同时进行删除或者添加操作时需要谨慎,一般使用迭代器进行操作。

1.2 foreach字节码分析

Java foreach 语法是在 JDK 1.5 时加入的新特性,主要是当作 for 语法的一个增强,那么它的底层到底是怎么实现的呢?

下面我们来好好研究一下:

foreach 语法内部,对 collection 是用 iterator 迭代器来实现的,对数组是用下标遍历来实现。Java 5 及以上的编译器隐藏了基于 iteration 和数组下标遍历的内部实现

注意:这里说的是Java 编译器Java 语言对其实现做了 隐藏,而不是某段 Java 代码对其实现做了隐藏,也就是说,我们在任何一段 JDKJava 代码中都找不到这里被隐藏的实现。这里的实现,隐藏在了Java 编译器中,查看一段 foreachJava 代码编译成的字节码,从中揣测它到底是怎么实现的了。

我们写一个例子来研究一下:

public class HashMapIteratorDemo {
    String[] arr = {
        "aa",
        "bb",
        "cc"
    };

    public void test1() {
        for (String str: arr) {}
    }
}

将上面的例子转为字节码反编译一下(主函数部分):


在这里插入图片描述

也许我们不能很清楚这些指令到底有什么作用,但是我们可以对比一下下面段代码产生的字节码指令:

public class HashMapIteratorDemo2 {
    String[] arr = {
        "aa",
        "bb",
        "cc"
    };

    public void test1() {
        for (int i = 0; i < arr.length; i++) {
            String str = arr[i];
        }
    }
}

在这里插入图片描述

看看两个字节码文件,发现指令几乎相同,如果还有疑问我们再看看对集合的 foreach 操作:

通过 foreach 遍历集合:

public class HashMapIteratorDemo3 {
    List < Integer > list = new ArrayList < > ();
    
    public void test1() {
        list.add(1);
        list.add(2);
        list.add(3);

        for (Integer
            var: list) {}
    }
}

通过 Iterator 遍历集合:

public class HashMapIteratorDemo4 {
    List < Integer > list = new ArrayList < > ();

    public void test1() {
        list.add(1);
        list.add(2);
        list.add(3);

        Iterator < Integer > it = list.iterator();
        while (it.hasNext()) {
            Integer
            var = it.next();
        }
    }
}

将两个方法的字节码对比如下:


在这里插入图片描述
在这里插入图片描述

我们发现两个方法字节码指令操作几乎一模一样;
这样我们可以得出以下结论:
对集合来说,由于集合都实现了 Iterator 迭代器,foreach 语法最终被编译器转为了对 Iterator.next() 的调用;对于数组来说,就是转化为对数组中的每一个元素的循环引用。

1.3 HashMap 遍历集合并对集合元素进行 remove、put、add

1.3.1 现象

根据以上分析,我们知道 HashMap 底层是实现了 Iterator 迭代器的 ,那么理论上我们也是可以使用迭代器进行遍历的,这倒是不假,例如下面:

public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");

        for (Map.Entry < Integer, String > entry: map.entrySet()) {
            int k = entry.getKey();
            String v = entry.getValue();
            System.out.println(k + " = " + v);
        }
    }
}

输出:


在这里插入图片描述

OK,遍历没有问题,那么操作集合元素 remove、put、add 呢?

public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");

        for (Map.Entry < Integer, String > entry: map.entrySet()) {
            int k = entry.getKey();
            if (k == 1) {
                map.put(1, "AA");
            }
            String v = entry.getValue();
            System.out.println(k + " = " + v);
        }
    }
}

执行结果:

在这里插入图片描述

执行没有问题,put 操作也成功了。

我们知道 HashMap 是一个线程不安全的集合类,如果使用 foreach 遍历时,进行addremove 操作会 java.util.ConcurrentModificationException异常。put 操作可能会抛出该异常。

1.3.2 异常原因

为什么会抛出这个异常呢?
我们先去看一下 Java API 文档对 HasMap 操作的解释吧。

在这里插入图片描述

翻译过来大致的意思就是:

该方法是返回此映射中包含的键的集合视图。
集合由映射支持,如果在对集合进行迭代时修改了映射(通过迭代器自己的移除操作除外),则迭代的结果是未定义的。集合支持元素移除,通过 Iterator.removeset.removeremoveAllretainalclear 操作从映射中移除相应的映射。简单说,就是通过 map.entrySet() 这种方式遍历集合时,不能对集合本身进行 removeadd 等操作,需要使用迭代器进行操作。
对于 put 操作,如果这个操作时替换操作如上例中将第一个元素进行修改,就没有抛出异常,但是如果是使用 put 添加元素的操作,则肯定会抛出异常了。我们把上面的例子修改一下:

public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");

        for (Map.Entry < Integer, String > entry: map.entrySet()) {
            int k = entry.getKey();
            if (k == 1) {
                map.put(4, "AA");
            }
            String v = entry.getValue();
            System.out.println(k + " = " + v);
        }
    }
}

执行出现异常:


在这里插入图片描述

这就是验证了上面说的 put 操作可能会抛出java.util.ConcurrentModificationException 异常。

但是有疑问了,我们上面说过 foreach 循环就是通过迭代器进行的遍历啊?为什么到这里是不可以了呢?

这里其实很简单,原因是我们的遍历操作底层确实是通过迭代器进行的,但是我们的 remove 等操作是通过直接操作 map 进行的,
如上例子:map.put(4, "AA"); 这里实际还是直接对集合进行的操作,而不是通过迭代器进行操作。
所以依然会存在 ConcurrentModificationException 异常问题

1.3.2 细究底层原理

我们再去看看 HashMap 的源码,通过源代码,我们发现集合在使用 Iterator 进行遍历时都会用到这个方法:

final Node < K, V > nextNode() {
    Node < K, V > [] t;
    Node < K, V > e = next;
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    if ((next = (current = e).next) == null && (t = table) != null) {
        do {} while (index < t.length && (next = t[index++]) == null);
    }
    return e;
}

这里 modCount 是表示 map 中的元素被修改了几次(在移除,新加元素时此值都会自增),而 expectedModCount 是表示期望的修改次数,在迭代器构造的时候这两个值是相等,如果在遍历过程中这两个值出现了不同步就会抛出 ConcurrentModificationException 异常。

现在我们来看看集合 remove 操作:
HashMap 本身的 remove 实现:

在这里插入图片描述

public V remove(Object key) {
    Node < K, V > e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

HashMap.KeySet 的 remove 实现

public final boolean remove(Object key) {
    return removeNode(hash(key), key, null, false, true) != null;
}

HashMap.EntrySet 的 remove 实现

public final boolean remove(Object o) {
    if (o instanceof Map.Entry) {
        Map.Entry << ? , ? > e = (Map.Entry << ? , ? > ) o;
        Object key = e.getKey();
        Object value = e.getValue();
        return removeNode(hash(key), key, value, true, true) != null;
    }
    return false;
}

HashMap.HashIterator 的 remove 方法实现

public final void remove() {
    Node < K, V > p = current;
    if (p == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    current = null;
    K key = p.key;
    removeNode(hash(key), key, null, false, false);
    expectedModCount = modCount; //--这里将expectedModCount 与modCount进行同步
}

以上四种方式都通过调用 HashMap.removeNode 方法来实现删除key的操作。在 removeNode 方法内只要移除了 keymodCount 就会执行一次自增操作,此时 modCount 就与 expectedModCount 不一致了;

final Node < K, V > removeNode(int hash, Object key, Object value,
    boolean matchValue, boolean movable) {
    Node < K, V > [] tab;
    Node < K, V > p;
    int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        ...
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode < K, V > ) node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount; //----这里对modCount进行了自增,可能会导致后面与expectedModCount不一致
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

上面三种 remove 实现中,只有第三种 iteratorremove 方法在调用完 removeNode 方法后同步了 expectedModCount 值与 modCount 相同,所以在遍历下个元素调用 nextNode 方法时,iterator 方式不会抛异常。

所以,如果需要对集合遍历时进行元素操作需要借助 Iterator 迭代器进行,如下:

public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");

        Iterator < Map.Entry < Integer, String >> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry < Integer, String > entry = it.next();
            int key = entry.getKey();
            if (key == 1) {
                it.remove();
            }
        }
    }
}

参考链接:https://mp.weixin.qq.com/s/52ppeM01FbAKELLLOFmhgQ

原文链接:https://www.jianshu.com/p/9cfc240df690

    在kubernetes中,所有的内容都抽象为资源,用户需要通过操作资源来管理kubernetes。

    kubernetes的本质上就是一个集群系统,用户可以在集群中部署各种服务,所谓的部署服务,其实就是在kubernetes集群中运行一个个的容器,并将指定的程序跑在容器中。

    kubernetes的最小管理单元是pod而不是容器,所以只能将容器放在Pod中,而kubernetes一般也不会直接管理Pod,而是通过Pod控制器来管理Pod的。

    Pod可以提供服务之后,就要考虑如何访问Pod中服务,kubernetes提供了Service资源实现这个功能。

    当然,如果Pod中程序的数据需要持久化,kubernetes还提供了各种存储系统。

对图片的解释:

pod控制器的作用是产生很多pod,pod中运行容器,容器中运行程序。

如果程序需要数据存储的话,那么就会有很多数据存储卷(volume),其中configmap、pvc、secret是为了存储数据中的资源。

pod要想外部进行访问,kubernetes提供了service代理。外部通过访问service就能访问pod了。

原文链接:https://www.jianshu.com/p/f978516a4eb7

题目链接:https://leetcode.cn/problems/add-one-row-to-tree/

问题描述:

给定一个二叉树的根 root 和两个整数 valdepth ,在给定的深度 depth 处添加一个值为 val 的节点行。

注意,根节点 root 位于深度 1

加法规则如下:

  • 给定整数 depth,对于深度为 depth - 1 的每个非空树节点 cur ,创建两个值为 val 的树节点作为 cur 的左子树根和右子树根。
  • cur 原来的左子树应该是新的左子树根的左子树。
  • cur 原来的右子树应该是新的右子树根的右子树。
  • 如果 depth == 1意味着 depth - 1 根本没有深度,那么创建一个树节点,值 val作为整个原始树的新根,而原始树就是新根的左子树。

示例 1:

image.png
输入: root = [4,2,6,3,1,5], val = 1, depth = 2
输出: [4,1,1,2,null,null,6,3,1,5]

示例 2:

image.png
输入: root = [4,2,null,3,1], val = 1, depth = 3
输出:  [4,2,null,1,1,3,null,null,1]

提示:

  • 节点数在 [1, 104] 范围内
  • 树的深度在 [1, 104]范围内
  • -100 <= Node.val <= 100
  • -105 <= val <= 105
  • 1 <= depth <= the depth of tree + 1

解法:递归

刷了这么多二叉树的问题,总结下来,遇到二叉树问题,一多半都是递归解法。

递归算法的思路:

(1)决定问题规模的参数。需要用递归算法解决的问题,其规模通常都是比较大的,在问题中决定规模大小(或问题复杂程度)的量有哪些?把它们找出来。
(2)问题的边界条件及边界值。在什么情况下可以直接得出问题的解?这就是问题的边界条件及边界值。
(3)解决问题的通式。把规模大的、较难解决的问题变成规模较小、易解决的同一问题,需要通过哪些步骤或等式来实现?这是解决递归问题的难点。把这些步骤或等式确定下来。

分析:

根据题目描述,我们可以找到三个边界条件:

(1)当树本身就为空时,那么结果一定也为空

(2)当要插入的深度是1时,及新建根节点,将之前的根节点作为新结点的左子树。

(3)当要插入的深度是2时,及给根节点新建左右两个子节点,然后再分别将之前根节点的左子树作为新建的左节点的左子树,将之前根节点的右子树作为新建的右节点的右子树。

问题递归演变:

当要插入的深度depth大于2时,意味着,先要要找到depth-1的所有结点,再给这些结点分别创建左右结点,再将之前的左右子树作为新节点的子节点,类似于上述第三个边界条件。

而树的遍历,可以使用递归算法,每遍历一次,根节点指向子节点,并让插入的深度减一,逐步使问题走向边界,当深度为2时,就达到了之前的边界三的条件,退出递归。

代码:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode addOneRow(TreeNode root, int val, int depth) {
        if(root == null) {
            return null;
        }

        if(depth == 1) {
            return new TreeNode(val,root,null);
        } 
        if(depth == 2) {
            root.left = new TreeNode(val,root.left,null);
            root.right = new TreeNode(val,null,root.right);
            return root;
        } else {
            root.left = addOneRow(root.left,val,depth-1);
            root.right = addOneRow(root.right,val,depth-1);
            return root;
        }
    }
}

原文链接:https://www.jianshu.com/p/f1fca80bb946

所用版本:

  • 处理器: Intel Core i9
  • MacOS 12.3.1
  • Xcode 13.3.1
  • objc4-838



熟悉类加载前, 先看下类的初始化方法_objc_init( 留意看下下面的注释 ):

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?

    // 环境变量初始化 
    environ_init();
    // 线程处理
    tls_init();
    // 运行C++静态构造函数。
    static_init();
    // runtime运行时初始化
    runtime_init();
    // objc异常处理系统初始化
    exception_init();
#if __OBJC2__
    // 缓存初始化
    cache_t::init();
#endif
    // 启动回调机制
    _imp_implementationWithBlock_init();
   // dyld通知注册
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
_objc_init

[ environ_init() ] 环境变量初始化

打印准备

再次运行可发现, 新增打印信息


打印信息

可看到打印了很多相关环境变量, OBJC_PRINT_IMAGES, OBJC_PRINT_CLASS_SETUP, OBJC_DISABLE_NONPOINTER_ISA等等。详细见: Xcode环境变量说明

[ tls_init ] 线程处理

针对本地线程处理做处理, 如果满足SUPPORT_DIRECT_THREAD_KEYS析构, 不满足初始化

tls_init

其中

// Thread keys 由libc保留供我们使用。
#   define SUPPORT_DIRECT_THREAD_KEYS 1 - 满足 0 - 不满足
  • 判断满足: pthread_key_init_np
    pthread_key_init_np
  • 判断不满足: tls_create
    tls_create

    重新初始化个线程key

[ static_init ] 运行C++静态构造函数。

如果有C++静态构造函数, libc会在dyld 调用_dyld_objc_notify_register之前, 先调用static_init执行。

static_init

举个例子, 我们写一个全局构造函数, 运行可发现

全局构造函数
打印结果

如图可看出, 在_dyld_objc_notify_register之前如果有静态C++构造函数, 那么通过static_init方法直接运行。

[ runtime_init ] 运行时初始化。

runtime_init

可看出主要分对两部分, 分类初始化类的表初始化进行

[ exception_init ] objc异常处理系统初始化

初始化libobjc的异常处理系统。其实是注册异常处理的回调,从而监控异常的处理

exception_init

举个例子:

例子

数组越界例子肯定会发生crash, 接着我们运行一下

先走了_objc_init中的exception_init

例子

执行old_terminate = std::set_terminate(&_objc_terminate);, 留意下此时还没有执行_dyld_objc_notify_register

例子

执行_dyld_objc_notify_registermain

例子

执行_objc_terminate

例子

最后crash
例子

其实当 crash发生时,会走_objc_terminate方法,接着走到uncaught_handler, 扔出异常并传入一个参数(e), 而e的回调往下看

`uncaught_handler `

e = fn

uncaught_handler

可看出将objc_uncaught_exception_handler fn(设置的异常) 赋值给uncaught_handler, 即 uncaught_handler 等于 fn, 由此可看出uncaught_handler, 本质是一个回调函数。

应用级crash

如图,系统其实会针对crash进行拦截处理,app会抛出一个异常句柄NSSetUncaughtExceptionHandler,传入一个函数给系统,当异常发生后,调用函数(函数中可以线程保活、收集并上传崩溃日志),然后回到原有的app层中,其本质是一个回调函数。

[cache_t::init()] 缓存初始化

缓存初始化

[ _imp_implementationWithBlock_init ] 启动回调机制

_imp_implementationWithBlock_init

[ _dyld_objc_notify_register ] dyld通知注册

首先可以看到_dyld_objc_notify_register(&map_images, load_images, unmap_image);
3个参数&map_imagesload_imagesunmap_image

  • &map_images: 映射整个镜像文件, 管理文件中, 动态库所有符号 (class, Protocol, selector, category)

先留意下&, 说明是指针传递, 传递是一个函数。这里用指针传递的好处是为了让map_images同步发生变化, 主要原因这个函数很重要, 苹果不希望它会因为一些重复加载发生错乱。同时这个映射操作也比较耗时, 如果不是一起的话, 也会增加耗时性。看下其内部

map_images

接下来看下map_images_nolock内部

map_images_nolock

代码有点长, 直接看重点代码: 读取镜像_read_images

_read_images

read_images这个方法很重要, 先说下_read_images都做了什么

_read_images

  • 条件控制进行一次加载
  • 修复预编译阶段的@selector混乱问题
  • 错误混乱的类处理
  • 修复重映射一些没有被镜像文件加载进来的类
  • 修复消息
  • 如果类里面有协议读取
  • 分类处理
  • 类的加载处理
  • 优化类
_read_images

接下来看下_read_images底层实现, 并依次看下上面内容

_read_images

① 第一次加载
doneOnce

略过一些代码看重点NXCreateMapTable

NXCreateMapTable

可看出第一次加载会创建一个表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

  • NXStrValueMapPrototype: 开辟类型
  • namedClassesSize: 开辟总容积

创建一张类的总表,这个表包含所有的类。4/3 因子这个我稍微讲一下 , 先了解哈希表负载因子一个概念

哈希表负载因子

  • 负载因子 = 总键值对数/数组的个数

  • 负载因子哈希表的一个重要属性,用来衡量哈希表的空/满程度,一定程度也可以提现查询的效率。负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。所以当负载因子大于某个常数(一般是0.75 即 3 / 4)时,哈希表自动扩容

  • 哈希表扩容时,一般会创建两倍于原来的数组长度,因此即使 key哈希值没有变化,对数组个数取余的结果会随着数组个数的扩容发生变化,因此键值对的位置都有可能发生变化,这个过程也成为重哈希(rehash)。

那么回来再看下, 表的大小也遵循负载因子,这里 namedClassesSize = totalClasses * 4 / 3相当于是负载因子``3/4的逆过程。namedClassesSize相当于总容量,totalClasses相当于要占用的空间。

例如我们想创建一张表 , 总容积: totalClass = x * 4 / 3
开辟表大小 x = totalClass * (3 / 4) = x * (4 / 3) * (3 / 4) = x = namedClassesSize

  1. 先看下gdb_objc_realized_classes:
    gdb_objc_realized_classes

其实gdb_objc_realized_classes是一张总表含所以类的表, 而runtime_init中的allocatedClasses

void runtime_init(void)
{
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}

allocatedClasses

可看出allocatedClasses只是一个alloc的分表. gdb_objc_realized_classes包含它。

② 修复预编译阶段的@selector
修复预编译阶段的@selector
  • sel是在dyld和llvm的时候加载的。
  • sels[i]是从mach-o获取的 mach-o会有相对内存地址和偏移地址。

sel 会有 名字 + 地址, 有些时候名字可能相同但是地址不相同, 这个时候需要修复一下


地址不相同

其中_getObjc2SelectorRefs是获取Mach-O中的静态段__objc_selrefs

GETSECT(_getObjc2SelectorRefs,        SEL,             "__objc_selrefs"); 

再看下sel_registerNameNoLock方法

sel_registerNameNoLock
__sel_registerName

调成一致, 将SEL覆盖到中namedSelectors集合Set对应位置上, 这里用Set原因: 虽然都是集合但是相比array, set处理hash方面效率确实是更高一些

举个例子: 比如你要存储元素A, 一个hash算法直接就能直接找到A应该存储的位置; 同样, 当你要访问A时, 一个hash过程就能找到A存储的位置. 而对于array,若想知道A到底在不在数组中, 则需要便利整个数组, 显然效率较低了;

综上: UnfixedSelectors修复sel就是把相不同的@selector统一化, 同时要以dyld的sel为准.

③ 错误/混乱类处理
混乱类处理

主要是从Mach-O中取出所有类,在遍历进行读取, 核心方法readClass
我们看下它的底层

[readClass]
readClass

先加一个打印, 看看都能读到什么类

 printf("%s - Test - %s \n", __func__, mangledName);
打印
打印结果

可看出能把系统类和自定义类都读取到, 没有用到的自定义类也会读取, 自定义类后添加的先读取。接下来我们跟一下自定义类的流程

    const char *SRTest = "SRTest";
  
    // 是否匹配
    if (strcmp(mangledName, SRTest) == 0) {
        printf("%s - 当前类 - %s \n", __func__, mangledName);
    }
自定义类

运行发现SRTest已进入

运行

先走修正方法


fixupBackwardDeployingStableSwift

如果类要求稳定, 那么会修正下不稳定的类


fixupBackwardDeployingStableSwift

接下来跟流程可发现会走addNamedClass

走addNamedClass

[addNamedClass]

稍微看下addNamedClass内部实现

addNamedClass

addNamedClass

addNamedClass将当前类添加到之前创建好的gdb_objc_realized_classes总表中

(之前有写, 往上翻第一次加载会创建一个表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);)

继续跟流程可发现走addClassTableEntry

[addClassTableEntry]
addClassTableEntry
addClassTableEntry

将类和元类插入allocatedClasses表中。这张表是在runtime_init中创建的。(之前也有写, 往上翻runtime_init )

void runtime_init(void)
{
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}

之后就会走readClass中的return cls;方法返回

综上,可看出readClass的主要将Mach-O中的类, 添加进内存 (插入到表中, 总表, alloc的分表都插一份)

readClass

原文链接:https://www.jianshu.com/p/bb70f62dc02b