1. 为什么值得花时间啃ORB-SLAM2的单目源码
如果你正在做视觉SLAM相关的开发,或者刚入门机器人感知方向,ORB-SLAM2大概率是你绕不开的一个名字。它在开源SLAM方案里的地位,有点像经典力学里的牛顿三定律——不是说它是唯一的方案,而是说它把很多基础问题定义清楚了,后来的很多工作都是在它的框架上做增量改进。单目版本又是整个ORB-SLAM2里最“纯粹”的一条线:没有深度相机帮你省掉尺度问题,没有IMU帮你扛快速运动,所有事情都得靠图像本身来推。把单目这条线吃透,再去看双目或者RGB-D版本,很多设计上的取舍你一眼就能明白为什么。
这篇是源码解读系列的第一篇,我不打算一上来就扎进某个函数的实现细节里,那样很容易迷路。更合理的做法是先搞清楚三件事:整个系统由哪些模块组成、数据在模块之间怎么流动、单目这条线跟另外两条线在架构上有什么本质区别。这三件事想清楚了,后面读代码就是按图索骥。适合的读者是已经跑过ORB-SLAM2的demo、大概知道它输出什么,但打开源码不知道从哪下手的人。如果你连编译都还没跑通,建议先把环境搭起来跑一遍再回来读,带着运行时的直观感受去读代码,效率会高很多。
我自己的习惯是,读任何SLAM源码之前先画一张数据流图,把“谁产生数据、谁消费数据、数据以什么形式存在”标清楚。ORB-SLAM2的代码量不算小,但它的模块边界划得很干净,画完图之后你会发现核心链路其实就那么几条。下面我就按这个思路,从整体架构开始,一层一层往里剥。
2. 整体架构与模块划分
2.1 三个线程各干什么活
ORB-SLAM2最核心的设计决策是把系统拆成三个并行线程:Tracking线程、Local Mapping线程、Loop Closing线程。这个拆分不是随便定的,它对应的是SLAM问题里三种不同时间尺度的任务。
Tracking线程跑得最快,每来一帧图像就要处理一次,它的任务是:根据上一帧的位姿估计,在当前帧里找特征匹配,算出当前帧的相机位姿,然后决定这一帧是不是关键帧。你可以把它理解成“前台接待”,来一个客人就接待一个,快速给出初步判断。
Local Mapping线程处理的是关键帧,频率低得多,可能每来几个关键帧才触发一次。它要做的是:把新关键帧插入地图,做局部BA(Bundle Adjustment),剔除冗余的关键帧和地图点。这是“后台整理”,把前台收进来的信息规整到地图里。
Loop Closing线程频率最低,它检测的是“我是不是回到了之前来过的地方”。一旦检测到回环,就要做位姿图优化,把累积的漂移一次性抹平。这是“事后审计”,平时不怎么干活,但一旦发现问题就是大动作。
为什么要这么拆?因为如果所有事情都串行做,每来一帧你都要等局部BA跑完才能处理下一帧,实时性根本没法保证。拆成三个线程之后,Tracking线程可以一直以帧率运行,Local Mapping和Loop Closing在后台慢慢算,只要保证它们处理的数据是Tracking线程已经产出的就行。这里的关键是线程之间的数据同步,ORB-SLAM2用了大量的互斥锁来保护共享数据,读代码的时候你会频繁看到unique_lock<mutex>这类写法,不要跳过它们,这些锁的位置恰恰标明了模块之间的边界。
2.2 单目、双目、RGB-D三条线的架构差异
ORB-SLAM2的代码里,单目、双目、RGB-D三条线共用了一套主体框架,差异主要体现在三个地方:初始化方式、特征匹配策略、尺度处理。
双目和RGB-D因为能直接得到深度信息,初始化非常简单——第一帧就能三角化出带尺度的地图点,系统立刻进入正常跟踪状态。单目不行,单目第一帧没有任何深度信息,必须通过运动恢复结构的方式,在头两帧之间做特征匹配,用对极几何算出相对位姿,再三角化出第一批地图点。这个过程需要足够的平移量,如果相机只是原地旋转,初始化会失败。这就是为什么你跑单目demo的时候,经常需要先左右移动一下相机,系统才会“启动”。
特征匹配策略上,双目可以在左右目之间做立体匹配,匹配搜索范围是受极线约束的,速度快且误匹配少。单目没有这个条件,只能在帧间做匹配,搜索范围大得多,所以单目版本对特征描述子的区分度要求更高。
尺度处理是单目最特殊的地方。双目和RGB-D的尺度是物理尺度,单位是米,地图点的距离是真实的。单目的尺度是归一化尺度,你没法知道一个地图点离相机是1米还是10米,整个地图可以任意缩放而不影响重投影误差。这意味着单目SLAM的轨迹精度评估必须做尺度对齐,也意味着单目系统在长时间运行后尺度会漂移。读单目源码的时候,你会看到很多地方做了归一化处理,比如Normalize()函数,这些在双目版本里是不需要的。
2.3 核心数据结构一览
在读具体流程之前,先把几个核心数据结构搞清楚,后面看代码会顺畅很多。
Map是整个地图的容器,里面存了所有的关键帧和地图点。它提供了一系列操作接口,比如AddKeyFrame、AddMapPoint、EraseKeyFrame等。Map本身不复杂,但它是三个线程共享的,所有对Map的修改都要加锁。
KeyFrame是关键帧对象,它存了这帧的位姿、特征点、描述子、以及它观测到的地图点。关键帧之间的连接关系通过共视图(Covisibility Graph)维护,如果两个关键帧观测到足够多的共同地图点,它们之间就有一条边,边的权重是共同观测的数量。共视图是局部BA和回环检测的基础。
MapPoint是地图点,存了世界坐标、平均观测方向、描述子、以及观测到它的关键帧列表。一个地图点被哪些关键帧看到,这个信息在三角化和BA里都要用。
Frame是普通帧,跟KeyFrame的区别是它不会被插入地图,只是Tracking线程内部用来做位姿估计的临时对象。Frame里也存特征点和描述子,但不参与长期地图维护。
这几个数据结构之间的关系,你可以这样理解:Frame是流水线上的工件,KeyFrame是质检合格后入库的成品,MapPoint是成品上的零件,Map是仓库,共视图是零件和成品之间的关联索引。Tracking线程负责把Frame加工成KeyFrame,Local Mapping线程负责入库和整理,Loop Closing线程负责定期盘点。
3. 单目初始化的核心逻辑
3.1 为什么单目初始化这么麻烦
单目初始化的本质问题是:从两张没有深度的图像里,同时恢复相机运动和场景结构。这是一个欠约束问题,因为如果你把场景整体缩放一倍,同时把相机平移也缩放一倍,重投影结果完全一样。所以单目初始化只能恢复出相对尺度下的结构和运动,绝对尺度是丢失的。
ORB-SLAM2的单目初始化采用了两步策略:先算单应矩阵,再算基础矩阵,然后根据评分选一个更好的模型来恢复位姿。为什么要算两个模型?因为场景结构不同,适用的模型不同。如果场景主要是一个平面(比如对着墙面、桌面),单应矩阵更稳定;如果场景有明显的深度层次(比如室内有前后景),基础矩阵更合适。两个都算一遍,用启发式评分选一个,比只算一个鲁棒得多。
3.2 特征提取与匹配的细节
初始化之前,首先要对前两帧做特征提取。ORB-SLAM2用的是ORB特征,它结合了FAST角点检测和BRIEF描述子,速度比SIFT快一个数量级,精度又比单纯的FAST好很多。提取的时候会做金字塔分层,每层提取固定数量的特征点,这样做的目的是让特征点分布更均匀,同时给后续的尺度不变性提供基础。
特征提取完之后要做匹配。单目初始化阶段的匹配是暴力匹配,对第一帧的每个特征点,在第二帧里找描述子距离最近的那个。但暴力匹配误匹配很多,所以后面要跟一个交叉验证:第一帧的特征点A匹配到第二帧的B,那B反向匹配回来是不是也匹配到A?只有双向都一致的才保留。这个操作能把误匹配率降下来一大截。
匹配完之后还要做RANSAC,因为即使交叉验证之后,仍然会有一些错误的匹配。RANSAC的思路是:随机选一组最小样本集(比如8个点对)算一个模型,然后看有多少其他点符合这个模型,重复多次选内点最多的那个模型。单目初始化里,单应矩阵和基础矩阵各做一次RANSAC,各自得到一组内点。
3.3 单应矩阵与基础矩阵的评分选择
两个模型都算完之后,怎么选?ORB-SLAM2用了一个启发式评分:分别计算两个模型的内点数和内点分布,然后给一个综合评分。评分高的模型胜出。如果两个模型评分接近,或者内点数太少,初始化就判定为失败,等待下一帧重新尝试。
这个评分机制的具体公式在源码里是一个叫CheckHomography和CheckFundamental的函数,里面用到了卡方检验的阈值。你不需要记住公式,但要理解它的设计意图:不是简单地比内点数,而是比内点的“质量”。一个模型如果内点很多但分布很集中(比如都在图像的一个小角落),它的评分会被压低,因为这种分布对位姿估计的约束不够。
选完模型之后,如果是单应矩阵胜出,就用ReconstructH恢复位姿;如果是基础矩阵胜出,就用ReconstructF。这两个函数内部都会做三角化,把匹配点对恢复成3D地图点。三角化的时候要注意视差角,视差角太小的点对深度估计非常敏感,误差很大,所以ORB-SLAM2会检查三角化后的重投影误差,误差太大的点会被剔除。
实操心得:单目初始化失败是新手最常遇到的问题。最常见的原因是相机运动不够——如果你只是拿着相机原地转,平移量太小,三角化根本算不出深度。正确的做法是先让相机有一个明显的平移,比如左右移动半米左右,同时保持场景中有足够的纹理。另外,如果场景太暗或者纹理太少,特征点数量不够,初始化也会失败。我一般会先对着一个有纹理的桌面或者书架做初始化,成功率比对着白墙高得多。
4. Tracking线程的完整流程
4.1 从一帧图像到相机位姿
Tracking线程的主函数是Track(),它每来一帧图像就被调用一次。整个流程可以分成几个阶段:特征提取、初始位姿估计、局部地图跟踪、新关键帧决策。
特征提取阶段跟初始化阶段类似,但多了一个提取区域掩码的机制。因为上一帧已经有一些特征点了,这一帧提取的时候会尽量避开上一帧特征点附近的区域,让特征点分布更均匀。这个细节在源码里是通过ExtractORB函数的一个参数控制的。
初始位姿估计有两种模式:恒速模型和参考关键帧模型。恒速模型假设相机运动是匀速的,用上一帧的速度预测当前帧位姿,然后用这个预测位姿把上一帧的地图点投影到当前帧,做匹配和优化。参考关键帧模型是在恒速模型失效时启用的,直接用最近的关键帧来做匹配。恒速模型更快,但遇到运动突变会失效;参考关键帧模型更稳,但计算量大一些。ORB-SLAM2的策略是优先用恒速模型,如果匹配点太少就切换到参考关键帧模型。
局部地图跟踪是Tracking线程最核心的部分。它把当前帧能看到的所有地图点(包括局部地图里的点)都投影到当前帧,做匹配,然后优化位姿。这里用到的优化是只优化位姿,不优化地图点,因为地图点已经在Local Mapping线程里优化过了,Tracking线程只需要在固定地图点的前提下求当前帧的最优位姿。这个优化问题用g2o求解,误差项是重投影误差。
4.2 关键帧的筛选标准
不是每一帧都会变成关键帧。如果每帧都插入地图,地图会迅速膨胀,BA的计算量会爆炸。ORB-SLAM2的关键帧筛选条件比较宽松,但有几个硬性要求:
- 距离上一次插入关键帧已经过了至少若干帧(避免关键帧太密集)
- 当前帧跟踪到的地图点数量足够多(说明跟踪质量好)
- 当前帧跟踪到的地图点中,有足够比例是最近新增的(说明场景有变化,需要更新地图)
这几个条件同时满足才会插入关键帧。源码里对应的函数是NeedNewKeyFrame(),里面还有对Local Mapping线程状态的检查——如果Local Mapping线程正在忙,关键帧插入会被推迟,避免地图更新跟不上。
注意事项:关键帧筛选策略直接影响SLAM系统的精度和速度。条件太松,地图膨胀,BA跑不动;条件太紧,地图更新不及时,跟踪容易丢失。ORB-SLAM2的默认参数是经过大量实验调出来的,如果你要改,建议先跑标准数据集看基线性能,再一点点调,不要一上来就大改。
4.3 跟踪丢失后的重定位机制
Tracking线程最怕的就是跟踪丢失——当前帧找不到足够的地图点匹配,位姿估计失败。ORB-SLAM2的处理方式是:先尝试重定位,如果重定位也失败,就进入丢失状态,等待系统重置。
重定位的思路是:把当前帧的特征点跟所有关键帧做匹配,用词袋模型加速搜索。词袋模型是事先离线训练好的,把描述子空间划分成若干聚类,每个特征点映射到一个词,一帧图像就变成一个词向量。匹配的时候先比较词向量的相似度,只跟相似度高的关键帧做详细匹配。这个机制让重定位不需要遍历所有关键帧,速度可以接受。
如果重定位成功,Tracking线程会从丢失状态恢复,继续正常跟踪。如果连续多帧重定位都失败,系统会判定为彻底丢失,需要重新初始化。这里有个细节:重定位成功之后,当前帧会被当作关键帧插入地图,因为重定位意味着相机位置发生了较大跳变,地图需要更新。
5. Local Mapping线程的地图维护
5.1 关键帧插入与地图点三角化
Local Mapping线程的主循环是Run(),它不断从关键帧队列里取新的关键帧,然后做一系列地图维护操作。第一步是处理新关键帧:把关键帧插入地图,更新共视图,然后对关键帧里还没三角化的特征点做三角化,生成新的地图点。
三角化的过程是:对当前关键帧的每个特征点,在共视关键帧里找匹配,如果找到足够的匹配,就用多视图几何三角化出3D点。三角化之后要检查对极约束和重投影误差,不满足条件的点会被剔除。这里有个经验:三角化出来的地图点,如果被至少两个关键帧观测到,且视差角足够大,质量才可靠。ORB-SLAM2会检查这些条件,不满足的点不会加入地图。
5.2 局部BA的优化策略
地图点三角化完之后,Local Mapping线程会做一次局部BA。局部BA的优化范围是:当前关键帧、跟当前关键帧共视关系最强的一批关键帧、以及这些关键帧观测到的所有地图点。优化变量是关键帧位姿和地图点坐标,误差项是重投影误差。
为什么只做局部BA而不是全局BA?因为全局BA的计算量太大,没法在线运行。局部BA只优化跟当前关键帧最相关的一部分,既能改善局部精度,又能控制计算量。全局BA只在回环检测之后做一次,用来消除累积漂移。
局部BA用g2o求解,里面有个细节:固定关键帧。局部BA的优化窗口里,有一些关键帧是固定的(不优化位姿),这些通常是跟当前关键帧共视关系较弱、但在优化窗口边缘的关键帧。固定它们是为了防止优化窗口整体漂移,给优化问题提供一个参考基准。
5.3 冗余关键帧与地图点的剔除
局部BA之后,Local Mapping线程会做一轮剔除:把冗余的关键帧和地图点删掉。什么样的关键帧算冗余?如果一帧的关键帧观测到的地图点,有90%以上也能被其他至少三个关键帧观测到,那这帧就是冗余的,可以删掉。什么样的地图点算冗余?如果一个地图点被观测到的次数太少,或者被观测到的关键帧之间共视关系太强(说明这个点没有提供新的约束),那它也可以删掉。
这个剔除机制是ORB-SLAM2能长时间运行的关键。没有剔除,地图会无限膨胀,BA越来越慢,最终系统卡死。剔除之后,地图保持在一个合理的规模,BA的计算量可控。但剔除也不能太激进,否则地图点太少,跟踪容易丢失。ORB-SLAM2的剔除阈值是经过调优的,源码里对应的函数是KeyFrameCulling()。
实操心得:如果你发现SLAM系统跑着跑着跟踪就丢了,可以检查一下地图点的数量。如果地图点数量一直很少,可能是剔除太激进了,可以适当放宽剔除阈值。反过来,如果地图点数量爆炸式增长,BA越来越慢,可以收紧剔除阈值。这个参数没有绝对的最优值,跟场景有关,室内场景和室外场景的最优值不一样。
6. Loop Closing线程与全局优化
6.1 回环检测的词袋模型
Loop Closing线程的核心任务是检测回环。回环检测的难点是:怎么在不知道相机真实轨迹的情况下,判断当前帧是不是回到了之前来过的地方。ORB-SLAM2用的是词袋模型(Bag of Words)。
词袋模型的原理是:离线阶段,用大量图像训练一个视觉词典,词典里每个“词”是一个描述子聚类中心。在线阶段,把一帧图像的所有描述子映射到最近的词,统计每个词出现的频率,就得到一个词向量。两帧图像的相似度可以用词向量的余弦相似度来衡量。如果当前帧跟某个历史关键帧的词向量相似度很高,就认为可能发生了回环。
词袋模型的好处是速度快,因为相似度计算只是向量运算,不需要逐特征匹配。但它的缺点是会丢失空间信息,两帧图像如果词向量相似但空间布局完全不同,也会被误判为回环。所以词袋模型只做初筛,筛出来的候选帧还要做几何验证,确认确实是回环。
6.2 回环校正与位姿图优化
回环检测确认之后,Loop Closing线程会做回环校正。校正的过程是:先计算当前帧跟回环帧之间的相对位姿,然后把这个相对位姿作为约束,加入到位姿图里做优化。位姿图的节点是关键帧位姿,边是关键帧之间的相对位姿约束。回环约束加入之后,位姿图优化会把累积的漂移分摊到整个轨迹上,让轨迹闭合。
位姿图优化之后,还要做一次全局BA,把地图点的坐标也一起优化。全局BA的计算量很大,ORB-SLAM2把它放在一个单独的线程里跑,不阻塞主流程。全局BA跑完之后,整个地图的精度会提升一个档次。
这里有个细节:回环校正之后,当前帧的位姿会发生变化,Tracking线程需要知道这个变化,否则下一帧的位姿估计会基于旧的位姿,导致跟踪失败。ORB-SLAM2的处理方式是:回环校正之后,把当前帧的位姿更新到校正后的值,Tracking线程下一帧会基于这个新位姿做预测。
6.3 本质图与全局BA的取舍
ORB-SLAM2在回环校正之后,做全局BA之前,会先做一次本质图优化。本质图是共视图的一个子图,只保留共视关系最强的那些边。本质图优化的计算量比全局BA小得多,但能快速把回环误差分摊开。全局BA在本质图优化之后跑,进一步精化地图。
为什么要分两步?因为全局BA很慢,如果直接跑全局BA,回环校正的实时性没法保证。本质图优化先快速把误差分摊开,让轨迹大致闭合,然后全局BA慢慢精化。这个设计体现了ORB-SLAM2的一贯思路:快速响应,慢速精化。
注意事项:回环检测的阈值设置很关键。阈值太高,回环检测不到,漂移一直累积;阈值太低,误检回环,地图会被错误地“拉”到一起,精度反而下降。ORB-SLAM2的默认阈值在大多数场景下表现不错,但如果你的场景特别大或者特别小,可能需要调整。我一般会先跑一遍看回环检测的日志,如果发现误检,就提高阈值;如果发现漏检,就降低阈值。
7. 读源码时容易踩的坑与排查技巧
7.1 线程同步与锁的常见问题
ORB-SLAM2的代码里到处都是锁,读代码的时候很容易被这些锁绕晕。我的建议是:先不要管锁,把每个线程的主流程读清楚,然后再回头看锁的位置,理解每个锁保护的是什么数据。锁的位置通常对应着模块之间的数据交接点,理解了数据交接,锁的作用就清楚了。
常见的坑是死锁。如果你修改代码的时候不小心改变了锁的获取顺序,两个线程可能互相等待对方的锁,导致死锁。ORB-SLAM2的锁获取顺序是经过设计的,修改的时候要特别小心。一个实用的技巧是:在锁的获取和释放处加日志,跑一遍看有没有异常。
7.2 特征点数量与跟踪稳定性的关系
特征点数量直接影响跟踪稳定性。ORB-SLAM2默认每帧提取1000个特征点,这个数量在大多数场景下够用,但在纹理特别丰富的场景下可能不够,在纹理特别少的场景下又太多(很多是噪声)。如果你发现跟踪容易丢,可以尝试增加特征点数量,但要注意计算量会上升。
特征点提取还有一个参数是金字塔层数,默认是8层。层数越多,尺度不变性越好,但计算量也越大。如果你的场景尺度变化不大(比如室内),可以减少层数来提速;如果场景尺度变化大(比如从近处走到远处),增加层数会更稳。
7.3 地图点数量异常增长的排查思路
地图点数量异常增长是新手常遇到的问题。原因可能有几个:一是关键帧插入太频繁,导致地图点三角化太多;二是剔除机制没生效,冗余地图点没被删掉;三是回环检测误检,导致地图被错误地扩展。
排查的思路是:先看关键帧数量,如果关键帧数量增长很快,说明关键帧筛选太松;再看地图点数量跟关键帧数量的比例,正常情况下一个关键帧对应100-200个地图点,如果比例远高于这个,说明三角化太激进或者剔除没生效;最后看回环检测日志,如果有大量回环被检测到但几何验证没通过,说明词袋模型阈值太低。
7.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 初始化失败 | 相机平移不足或场景纹理太少 | 检查初始化时的匹配点数量和视差角 | 增加相机平移量,换纹理丰富的场景 |
| 跟踪频繁丢失 | 特征点数量不足或地图点太少 | 查看每帧跟踪到的地图点数量 | 增加特征点提取数量,放宽剔除阈值 |
| 地图点爆炸增长 | 关键帧插入太频繁或剔除失效 | 统计关键帧数量和地图点数量 | 收紧关键帧筛选条件,检查剔除逻辑 |
| 回环误检 | 词袋模型阈值太低 | 查看回环检测日志中的相似度分数 | 提高回环检测阈值,加强几何验证 |
| BA越来越慢 | 地图规模太大 | 查看局部BA的优化窗口大小 | 加强剔除,限制局部BA的关键帧数量 |
| 轨迹尺度漂移 | 单目固有缺陷 | 对比轨迹与真值的尺度 | 无法根本解决,只能通过回环校正缓解 |
这张表是我自己在调试ORB-SLAM2时总结的,不一定覆盖所有情况,但常见的坑基本都在里面了。实际排查的时候,日志是最好的朋友,ORB-SLAM2的日志输出很详细,关键信息都在里面,学会看日志能省很多时间。
8. 单目源码阅读的进阶路线
8.1 从Tracking到Local Mapping的阅读顺序
读源码最忌讳跳着读。我的建议是按数据流的方向读:先读Tracking线程,理解一帧图像怎么变成位姿估计;再读Local Mapping线程,理解关键帧怎么进入地图、地图怎么维护;最后读Loop Closing线程,理解回环怎么检测和校正。这个顺序跟数据产生的顺序一致,读起来最自然。
每个线程内部,先读主函数(Track()、Run()、Run()),把主流程搞清楚,再读被调用的子函数。子函数不用每个都细读,先读那些名字里带Compute、Estimate、Optimize的,这些是核心计算;名字里带Update、Insert、Erase的可以稍后再看,这些是数据维护。
8.2 关键函数与调用关系梳理
ORB-SLAM2的核心函数其实不多,我列几个最重要的:
Tracking::Track():Tracking线程主循环Tracking::TrackWithMotionModel():恒速模型跟踪Tracking::TrackReferenceKeyFrame():参考关键帧跟踪Tracking::Relocalization():重定位LocalMapping::Run():Local Mapping线程主循环LocalMapping::LocalBundleAdjustment():局部BALoopClosing::Run():Loop Closing线程主循环LoopClosing::DetectLoop():回环检测LoopClosing::CorrectLoop():回环校正
这几个函数理解了,整个系统的骨架就清楚了。其他函数都是围绕这几个核心函数的辅助逻辑,用到的时候再查就行。
8.3 从单目扩展到双目的学习路径
单目源码读透之后,扩展到双目会容易很多。双目的差异主要在初始化(直接用立体匹配得到深度)、特征匹配(增加左右目匹配)、尺度处理(物理尺度)。你可以先读双目的初始化部分,对比单目的初始化,理解两种方式的取舍;再读双目的特征匹配部分,理解极线约束怎么加速匹配;最后读双目的尺度处理部分,理解为什么双目不需要归一化。
这个扩展过程本身就是很好的学习材料,因为你能看到同一个问题在不同传感器配置下的不同解法,这种对比能加深你对SLAM本质问题的理解。
我个人在实际读源码过程中的体会是,不要试图一次读懂所有细节。第一遍读的时候,把主流程搞清楚就行,遇到看不懂的函数先跳过,标记下来,等主流程清楚了再回头看。第二遍读的时候,重点看那些标记的函数,理解它们在整个流程里的作用。第三遍读的时候,再抠细节,比如某个参数为什么这么设、某个判断条件为什么这么写。三遍下来,基本就吃透了。另外,读源码的时候一定要配合跑实验,改一个参数跑一遍看效果,比干读代码印象深得多。