前言
CocoaPods 云端分析能力是字节跳动的终端技术团队 (Client Infrastructure) 下 Developer Tools 部门提供的一系列云化基础设施之一, Developer Tools 团队致力于建设下一代移动端云化基础设施,团队通过云 IDE 技术、分布式构建、编译链接等技术,优化公司各业务的研发和交付过程中的质量、成本、安全、效率和体验。
一、背景
iOS 组件化研发模式下,CocoaPods 已然成为 iOS 业界标准的依赖管理工具。但随着业务能力不断拓展迭代,组件数量不断增多,导致 App 工程复杂度急剧增大,依赖管理效率严重下降,甚至出现潜在的稳定性问题。为了能够更快、更稳定得管理大型项目的组件依赖,iOS build 部门打造了一套中心化依赖管理服务 —— 云端依赖分析,从工具链的层面收敛了依赖管理流程,加速了决议速度,聚合了失败问题。
二、什么是云端依赖分析
基于 CocoaPods 的 iOS 工程管理,每次执行 pod install,都需要先将组件索引信息 Spec 仓库同步到本地,一般都依靠于 git 仓库的 clone,然后读取 Podfile、Lockfile 以及其他配置文件,开始进入依赖分析、依赖下载、工程整合等几个步骤。
云端分析是一个依赖于字节跳动自研制品库平台,通过工具链上传本地工程构建物料,快速返回依赖分析结果,中心化管理 iOS 工程依赖的云端服务。云端分析服务会依赖于制品库提供所有组件索引信息;并且通过云端分析本地工具在环境准备过程中获取本地工程物料,统一上传至云端进行依赖决议任务,云端借助于一系列优化手段以及服务器性能,快速返回一个决议结果,本地接收到决议结果之后进行后续的依赖下载与工程整合过程。
云端分析的接入方式也极其容易,不需要增加配置文件,也不需要修改原有研发模式,以无侵入、无接入成本、不影响研发流程的方式接入到工程项目中。唯一需要做的,仅仅是在 CocoaPods 工具链中加入云端分析的 RubyGem 插件,并在 pod install 命令中增加一个开启优化功能的控制开关参数。
三、如何加速决议
3.1 制品库 (全量组件索引信息)
基于 Cocoapods 的 iOS 开发体系对 iOS 的产物管理是非常粗放的,直接将不同的 git 仓库作为构建产物(podspec 文件)的索引仓库,担当了制品库的角色。随着 iOS 工程的复杂化,git 仓库的文件信息增加导致组件索引信息查询困难,仓库的同步速度缓慢。BitNest 制品库是公司自研的移动端的产物管理系统,用于管理持续集成过程中所产生的构建产物。制品库将分离在各个 git 仓库的 podspec 源进行了中心化的管理,通过一套完整的 CLI 指令,能够快速拉取、查询 podspec 信息。云端分析服务借助于制品库能力的帮助,能够在云端实时访问一个全量完整的 podspec 源信息。每次 CocoaPods 任务都不需要再去更新 podspec 源信息,也不会因为不及时更新 podspec 源信息而找不到最新发版的组件 podspec 信息。
3.2 缓存机制
在介绍缓存机制之前,先简单介绍一下 pod install 中依赖分析的运行流程。在第一次执行的时候(忽略 lockfile),CocoaPods 会通过 DSL 从 Podfile 中读取具体的 plugin,source,target,pod 等内容,创建相应的对象完成准备阶段。在每个 Target 对象中每个 pod 都创建成了 Dependency 对象,并且都会有具体的 Requirements 对象。所有 Target 对象的所有 Dependency 对象都逐个被加入到堆栈中,并创建一个 Graph 依赖节点图。每个 Dependency 对象根据其 Requirements 去对应的 Source 仓库寻找对应的 pod,如果 Requirements 中没有仓库信息,就从 podfile 公共 Source 中遍历寻找。找到对应的 pod 之后,会先建立一个版本列表,并从版本列表中找出所有符合 Requirements 要求的 pod,然后读取对应是 podspec 文件内容。决议中会对 Spec 对象中隐式的 pod 创建新的 Dependency 加入到分析堆栈和 Graph 中。如果某个版本的 Spec 在遍历 Graph 依赖图时不满足另一个同名依赖的 Requirements,就会进行出栈回撤和依赖图回撤,直至所有 Dependency 都被找到对应的 Spec 对象为止,分析就完成了。可见,在 CocoaPods 依赖管理过程中,有大量重复的对象创建和排序查找过程,极大的降低了研发效率。试想,让 CocoaPods 任务所需的对象一直保持就绪状态,每当收到任务请求立即执行依赖分析工作,就可以快速返回结果。云端分析服务集中化了所有 CocoaPods 的依赖管理任务,针对重复的工作搭建了对象缓存机制。采用懒加载的模式,对新增对象进行缓存,在下一次任务进来之后立刻进入依赖决议过程。
3.2.1 排序 Version 缓存
在分析每个 pod 时,为了能获取最新版本的 pod 依赖,CocoaPods 会对 source 仓库中的所有版本号建立对应的 Version 对象,并进行排序。目前,公司内部大部分制品版本已经达到上万的数量级,而且在不指定 source 源的情况下,二进制版本和源码版本都会被排序并读取,最终获取一个满足要求且最新的版本。由于组件版本号都以 “.” 和 “-” 分段,大部分组件版本都存在 4 个或者 5 个字段以上。这也致使上万个组件在进行排序的过程中,每次排序对比都需要遍历 4 次以上,使时间复杂度提升了好几倍,极大得增加了耗时。
为了更快得获取到有序的版本列表,由制品库服务维护了所有 pod 组件从大到小排序的版本文件;每增加一个新的 pod 版本,制品库都会向文件中插入一个新版本;删除时,则会删除相应的版本字段。
有了有序的版本文件,云端分析增加 Version 缓存的主要目的是为了将版本分段信息一直维持在 Version 对象中,可以快速判断当前 Version 是否满足依赖的要求。Version 缓存可以让依赖管理过程提速大约 10-12 秒左右。
云端分析在无版本缓存的情况下,会优先读取版本文件中的数据,直接获得有序的版本列表;如果版本列表长度与 source 中组件版本目录长度不一致,会回退到原始方法(版本列表出错,确保分析的正确性)。在缓存命中的情况下,也需要判断缓存版本列表长度是否与 pod 版本目录长度相等(有新增版本,缓存未新增),则会从版本列表数组中查找出差异版本,并对缓存进行修正。
3.2.2 Spec 对象缓存
CocoaPods 在从排序版本中查找满足依赖要求的 podspec 时,会将所有满足依赖要求的 podspec 版本内容全部读取进来,进行依赖决议遍历。如果在不注明具体版本的情况下,所有版本的 podspec 文件都将被读取,并且在不注明具体 source 源的情况下,所有 source 存在的 pod 也都会被读取。一万个 podspec 文件读取就需要花费 30 秒左右(据不同磁盘而定) 。
云端分析会对每次分析任务 IO 读取的 podspec 文件内容进行缓存。在下次任务获取 Spec 对象时,可以根据 source,pod_name,version 三个字段直接得到对应的 Spec 对象。
同时,为了确保 Spec 的正确性,防止 Spec 在不改变版本而更改内容的情况出现。Spec 对象缓存是以一个多维数组的形式存在,通过判断 podspec 文件的修改时间,来更新缓存中的 podspec 内容为最新提交的,确保 checksum 计算与本地拉仓依赖分析的计算值相同,实现云端依赖分析的正确性。后续,也会增加 Spec 缓存命中次数,Spec 对象过期时间等,实现 Spec 缓存的清理策略。
3.2.3 缓存复用
云端分析也会对分析结果进行缓存,下一次遇到相同的分析任务能够直接复用。云端在获取一次物料之后,会对物料做一次全局 hash 计算和一次分段 hash 计算,分别缓存完整的分析结果和分析结果图 Graph。针对下一次分析任务,如果是完全相同的物料可以直接返回一个可用的完整分析结果;如果未匹配,会通过一些 target,platform 等信息计算出一级平台信息 key,来确定具体 app 信息;再对所有 target 下的组件依赖逐个计算 hash 值,获得二级 hash 数组 key,并对应一个分析结果图 Graph value;通过模糊匹配的方式对 hash 数组 key 进行匹配,匹配到依赖个数相同最多的相近图,来替换物料中的 locked_dependencies,来加速分析。当然,模糊匹配能力也有一定的局限性,无法对原本上传 lockfile 物料的分析任务进行加速。
3.3 物料剪枝
云端分析会将 CocoaPods 对象转变为字节流进行传输。具体的上传物料与分析结果具体如下:
- 上传物料
云端分析工具链会将 Podfile 对象、lockfile 生成的 Molinillo Graph 对象、指定的 Source 对象、插件适配器,所有的外部源 Specs 对象(具体为指定 git,path 和 podspec 的 pre-release 对象)作为上传物料。但其实,云端分析并不需要这些本地对象的全部信息,可以对这些对象进行剪枝,例如 Podfile 对象仅需要 target_definitions 的链表即可;Molinillo Graph 对象仅需要所有 pod 对应的节点,而不需要记录操作节点的 log;Source 对象仅需要知道 name 和 repo_dir 即可,等等。其中,部分决议优化插件需要通过插件适配器额外传输一些配置 Config 对象。
- 结果返回
云端分析返回的结果为以 Target 为 key,相应的 Specs 数组为 value 的 hash 对象。结果返回之前,会先对所有 Spec 的 Source 进行剪枝。由于每个 Spec 对应的 Source 在后续流程中仅使用到 url 的字段进行分类与生成 lock 文件。因此,可以删除 Source 对象其他无用的字段,最小化传输内容,加快响应时间。对返回结果进行剪枝后,传输内容大小可以减少大约 10MB 以上。
3.4 决议策略兼容
为了确保决议结果的正确性和唯一性(single truth),云端分析兼容了字节跳动内部各 CocoaPods 决议策略优化的工具链。根据工程中构建配置参数,云端分析本地插件识别出具体的决议策略,并传递到云端分析服务器并激活对应决议策略算法进行快速决议。同时,结合已有的决议优化策略和云端的优化加速机制,让 CocoaPods 的依赖管理流程达到秒级返回。
四、总结
本文主要分享了目前字节跳动内部的一种 CocoaPods 云端化的优化方案,针对大量重复的 iOS 工程流水线构建任务进行了收敛和复用,在保证依赖决议正确性的前提下加速了依赖管理速率,提升了研发效能。目前云端分析服务已经完成第一阶段的开发并落地使用,已被公司内部几个核心的生产线使用。如头条接入云端分析服务后,pipeline 的依赖分析阶段耗时加速 60% 以上。后续,对于 CocoaPods 的下载优化,工程缓存服务也已经在技术探索中,相关技术文章将陆续分享,敬请期待!
像素旅人
0
0
0
专栏目录
财报数据可视化 —— pandas数据分析,pyecharts可视化
06-03
财报数据可视化 —— pandas数据分析,pyecharts可视化
中国企业国际化概念管理——目标管理方案.doc
02-13
中国企业国际化概念管理——目标管理方案.doc
德式时间管理——时间具体化.ppt
02-22
德式时间管理——时间具体化.ppt
Oracle 11g 对非结构化数据的管理——Secure Files.pptx
09-21
Oracle 11g 对非结构化数据的管理——Secure Files.pptx
金融监管信息化——需求分析说明书.docx
02-15
金融监管信息化——需求分析说明书.docx
C++类成员的访问权限以及类的封装
shiwei0813的博客
394
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是你能不能使用该类中的成员。Java、程序员注意,C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
简单着色器编写(上)
m0_74963816的博客
441
在OpenGL中,通过绑定和解绑缓冲区,你可以在渲染过程中使用不同的缓冲区数据来绘制不同的图形。在这个例子中,它告诉OpenGL如何解释顶点位置属性的数据:每个顶点位置由两个浮点数组成,不标准化,每个顶点属性在数组中的偏移量为0。因此,这句话就是在告诉OpenGL的画笔(顶点着色器):“嘿,我要用位置属性这个颜料盒(属性数组)中的颜色(数据)来绘制我的风景(模型)!这是因为在使用顶点缓冲区对象绘制时,你可以将多个顶点属性存储在不同的顶点属性数组中,然后使用。:这是指定顶点属性数组中每个顶点属性的字节数。
C++ DAY6
weixin_69028524的博客
288
每个具有虚函数的对象都有一个隐藏的成员,即虚指针(vptr),它在对象构造时自动设置,用于指向与该对象类型关联的虚函数表。————虚函数指在函数前加上virtual,虚函数满足继承(父类中该函数是虚函数),继承到子类中,该函数依旧是虚函数,如果之后继续继承,继承出的函数仍是虚函数。如果你想在创建汇聚子类的对象时初始化公共基类的数据成员,你需要在汇聚子类的构造函数初始化列表中显式调用公共基类的构造函数。父类的指针或者引用,指向或初始化子类的对象,调用子类对父类重写的函数,进而展开子类的功能。
c++ day4
qilitolxx的博客
1079
【代码】c++ day4。
C++day3(类、this指针、类中的特殊成员函数)
m0_68542867的博客
926
类、this指针、类中的特殊成员函数、定义一个矩形类(Rectangle),包含私有成员长(length)、宽(width),定义成员函数、构造函数允许函数重载、构造函数的初始化列表、需要显性定义出析构函数的情况、拷贝构造函数、深浅拷贝问题
【C++11】future和async等
慕雪华年的博客
1083
C++11的future和async等关键字more。
【设计模式】Head First 设计模式——观察者模式 C++实现
Friday
284
主题对象(出版者)管理某些数据,当主题内的数据改变,就会通知观察者(订阅者)。
c++多线程中常用的使用方法
qq_30143193的博客
210
线程中使用单例模式,互斥量的使用,互斥量与条件变量的使用,std::async函数模板与std::future类模板的使用,std::promise类模板与std::future类模板的使用; package_task的类模板的使用;
Leetcode刷题笔记--Hot31-40
牵一只蜗牛去散步
206
需要使用一个记录数组来标记当前 board[i][j] 是否被访问,回溯时还原访问状态;整体思路有点类似全排列,对于数组中的元素,加入(递归)或不加入(回溯)到记录数组中;不同于全排列的是,本题 dfs 的时候不需要重头遍历所有元素,整个加入过程是前向的;递归+回溯,遍历从board[i][j]出发,能否匹配给定的字符串;2--最小覆盖子串(76)5--柱状图中最大的矩形(84)1--颜色分类(75)1--颜色分类(75)4--单词搜索(79)
(内存池) 基于嵌入式指针的简单内存池
CUBElotus的博客
638
内存池百度百科 (baidu.com)(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池的实现方式多种多样,而本文仅实现一个简单的内存池,主要运用到嵌入式指针。嵌入式指针,指的在数据单元中,用一部分空间保存某一块空间的地址信息。实现方式多种多样。
iOS逆向:越狱及相关概念的介绍
397
iOS操作系统的封闭性一直是开发者们关注的焦点之一。为了突破Apple的限制,越狱技术应运而生。本文将深入探讨iOS越狱,包括可越狱的版本对比、完美越狱的概念、目前流行的越狱技术方案以及Cydia这一应用平台。
Linux知识点 -- Linux多线程(四)
最新发布
kissland96166的博客
216
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。thread.hpp 线程封装: lockGuard.hpp 锁的封装,构建对象时直接加锁,对象析构时自动解锁; log.hpp 注: (1)提取可变参数 使用宏来提取可变参数: 将可变
Pimpl模式
SNAKEpc12138的博客
300
类的常规实现和Pimpl实现各有优劣。若只是为了快速开发且没有对外隐藏需求,常规实现无疑是很好的选择,若想要减少编译依赖且不想对外展示私有成员,可选择使用Pimpl实现,代价就是开发及维护成本的提高。
【C++练习】普通方法+利用this 设置一个矩形类(Rectangle), 包含私有成员长(length)、 宽(width), 定义一下成员函数
Smallxu的博客
1122
设置一个矩形类(Rectangle), 包含私有成员长(length)、 宽(width), 定义成员函数://设置长度设置宽度void set wid(int w);获取长度: int get len();获取宽度: int get _wid);显示周长和面积: void show();
oFono学习笔记——modem初始化分析
06-08
oFono是一个开源的基于DBus的移动电话协议栈,它为各种不同的手机调制解调器(Modem)提供了一个统一的接口。在使用oFono之前,我们需要对Modem进行初始化配置,以确保Modem能够正常工作。 Modem初始化分为以下几个步骤: 1. 确定Modem的设备文件路径:通常情况下,Modem的设备文件路径为/dev/ttyUSB0或/dev/ttyACM0等串行端口设备。 2. 设置串口波特率:Modem的波特率通常为115200,我们需要设置串口波特率以确保与Modem正常通信。 3. 发送AT指令:AT指令是控制Modem的主要方式,我们需要发送一些AT指令来初始化Modem。常见的AT指令包括ATZ(恢复出厂设置)、ATE0(关闭回显)等。 4. 设置网络运营商APN:如果我们需要使用数据服务,需要设置正确的网络运营商APN。 5. 启动Modem:完成以上步骤后,我们需要向Modem发送启动指令以使其正常工作。 以上步骤可以通过oFono提供的API来实现。我们可以编写一个简单的初始化脚本,然后在系统启动时自动运行该脚本,以确保Modem能够正常工作。
“相关推荐”对你有帮助么?