Cocoa 文档注释与生成工具使用 - Jazzy + SourceKitten
source link: https://looseyi.github.io/post/tool/cocoa-documentation-2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
我们在前文 《Cocoa 代码注释与文档生成》 中详细介绍了如何为 Swift & ObjC 的代码编写符合规范的注释,以及使用 Jazzy 来生成项目文档。 今天我们来尝试一下,如何一键生成多个私有库的文档,并将其部署到 Github page 或者 Gitlab page 上。
本文知识目录
随着公司项目的迭代,一般都会沉淀出多个私有库。如果这些私有库可以够提供统一的文档查询和预览服务,那将有助于团队中的新成员快速了解业务。
作者所在的公司就维护者 20 多个的私有库,同时这些项目的代码注释完整度不一,注释的内容也参差不齐。如果我们可以通过这个在线文档,不仅可以提供快速的 API 查阅能力,也可以更好的监督和规范项目。
如何一键生成多依赖库的文档
我们先来简单分析一下要实现这个想法 💡需要做哪些事情。
- 现有的文档生成工具都是基于单个项目,而我们想要的是多依赖库的集合文档。那么就需要有一个索引页将各个依赖库串联起来,能够通过索引来访问它。
- 由于公司的项目是包含了 Swift & ObjC 混编的庞大项目,所维护的私有仓库不仅包含了纯 ObjC 和纯 Swift 实现的,还包括了 Swift & ObjC 混编代码的依赖库。所以需要支持这个三种场景。
- 生成的文档都是静态页面,需要将这些页面托管在静态资源服务上,关于这点 Github Page 和 Gitlab Page 就能解决。
- 毕竟项目是不断的迭代演进的,那如何在一定时机的情况下自动触发或者手动触发更新文档,也是十分重要的一件事情。
明确了我们要解决的问题,剩下的事情就简单了。
就直接使用 shell
将上面的步骤串联起来,如果大家熟悉其他语言也可以,文档生成工具就是 Jazzy
+ SourceKitten
。Jazzy 之前介绍过了,一起看看 SourceKitten 吧:
An adorable little framework and command line tool for interacting with SourceKit.
Sourcekitten 是基于 Apple 的 SourceKit 封装的命令行工具,SourceKitten 链接并与 sourcekitd.framework 通信以解析 Swift AST,提取 Swift 或 ObjC 项目的注释文档,获取 Swift 文件的语法数据等等。
SourceKit is a framework for supporting IDE features like indexing, syntax-coloring, code-completion, etc. In general it provides the infrastructure that an IDE needs for excellent language support.
文档索引页的生成
为了整体的样式统一,我们的索引页采用与 Jazzy
所生成的文档相同的 CSS 样式。由于 Jazzy
支持切换生成文档的主题,这里我们使用默认主题。
当我们访问静态网站时,入口一般都指向一个名为 index.html
的页面。 Jazzy
生成的入口也是 index.html
。
我们要做的就是往 index.html
内添加含对应的标签,并将标签链接指向各个依赖库的文档地址就可以了。
下面是我们需要修改的代码,完整的 index.html 模版可访问 Jazzy-template。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div class="content-wrapper">
...
<article class="main-content">
<section class="section">
<div class="section-content top-matter">
<h3 id='authors' class='heading'>业务库</h3>
</div>
</section>
<section class="section">
<div class="section-content">
<div class="task-group">
<ul class="item-container">
<li>token-business</li>
</ul>
</div>
</div>
</section>
<div class="section-content top-matter">
<h3 id='authors' class='heading'>基础库</h3>
</div>
<section class="section">
<div class="section-content">
<div class="task-group">
<ul class="item-container">
<li>token-base</li>
</ul>
</div>
</div>
</section>
</article>
</div>
要修改的就是上面的 <li>token-*</lib>
元素,这里留的默认 token 是为了方便替换。
由于业务库逻辑一般会比较多,如果和基础库文档放一起,可能会导致生成文档的太大,Github Page 无法正常解析。因此,需要单独的文档仓库来存放文档。
基础库生成的文档会统一放到项目的 docs
目录下,同时 <li>token-base</li>
标签的地址最后会指向 docs/$lib_name/index.html
目录。
目前的结构是这样的:
我们先来看一下以 Alamofire 项目生成的 docs
文档目录结构:
第一层包含了 Classes
、Enums
、Extensions
、Protocols
、Structs
等分类和对应的 index.html
索引文件。
第二层为具体到的每个 Class、Enum 或其他数据结构的 HTML 页面。如果该结构还存在嵌套的内部数据类型,会以递归的方式呈现。
整个 docs
的基础结构特别简单:
我们要做的就是复制上面的文件,以及修改的 index.html 就可以。
多依赖库的文档生成
对于 iOS 项目的依赖库管理标配为 CocoaPods (后面简称 Pod) ,它将所有的依赖库源码统一存放在项目的 /Pods
目录下。我们要做的就是遍历 /Pods
目录,逐一生成文档并将其输出到一个指定目录就可以了。
想法是美好的,现实是残酷的。在实际操作起来发现并没有那么简单。让我们开启踩坑之旅吧!
Swift 依赖库的文档生成
之前在 《Cocoa 代码注释与文档生成》 中介绍的 Swift 的文档生成都是基于该项目的 project
工程或者是 SwiftPM 配置来完成。好在 Pod 也为我们生成对应的 project
,我们仅需通过 --build-tool-arguments
来指定 project
和 target
就可以了。
从零开始,我们先新建一个 Demo.xcodeproj 并配置如下 Podfile:
1
2
3
4
target 'Demo' do
pod 'SnapKit'
pod 'AFNetworking'
end
调用 Jazzy 生成 Swift 库 SnapKit
的文档:
1
2
$ bundle exec jazzy -o docs/SnapKit \
--build-tool-arguments -project,Pods/Pods.xcodeproj,-target,SnapKit
通过 -o
将结果输出到 docs/SnapKit
目录下,执行后输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
Running xcodebuild
Parsing Constraint.swift (1/34)
...
Parsing UILayoutSupport+Extensions.swift (34/34)
`ConstraintLayoutSupport` has no USR. ...
9% documentation coverage with 239 undocumented symbols
included 264 public or open symbols
skipped 81 private, fileprivate, or internal symbols (use `--min-acl` to specify a different minimum ACL)
building site
building search index
jam out ♪♫ to your fresh new docs in `docs/SnapKit`
可以看到 Jazzy 会遍历项目下的每个 swift 文件,对于项目中未引用的代码也会有提示。最后会输出代码的注释覆盖率,SnapKit 的覆盖率为 9%,有 239 个未注释的符号或变量。
指定文档的范围
需要注意的是,Jazzy 可以通过 --min-acl
来控制输出文档的范围。
-
对于 Swift 项目,默认仅生成声明为
public
和open
的类、属性和方法等,如果想要输出私有变量的注释,还可以设置为internal
、fileprivate
或private
。 -
对于 ObjC 项目,Jazzy 仅会生成在
--umbrella-header
所指定的 header 文件中所引用的.h
文件。
ObjC 依赖库的文档生成
相比 Swift,Objc 的依赖库需要多处理 umbrella header
的问题。先看 AFNetworking 的文档生成命令:
1
2
3
4
5
6
7
8
9
10
$ lib_name=AFNetworking
lib_path=$(pwd)/Pods/$lib_name
umbrella_header="$lib_path/$lib_name/$lib_name-umbrella.h"
sdk_path=`xcrun --show-sdk-path --sdk iphonesimulator`
bundle exec jazzy -o docs/$lib_name \
--objc \
--sdk iphoneos \
--build-tool-arguments \
--objc,$umbrella_header,--,-x,objective-c,-isysroot,$sdk_path,-I,$lib_path
第一个是需要指定 --objc
,因为 Jazzy 默认解析 Swift 项目。
再来看 --build-tool-arguments
后跟的几个参数:
- –objc <umbrella-header PATH>:这里的
--objc
是通知 SourceKitten 我要解析的是 Objc 的头文件,后面紧跟的为依赖库的 umbrella header - –:作为分割符,表示之后的参数会转发到
xcodebuild
或swift build
- -x objective:通知
xcodebuild
或swift build
我要编译 ObjC 啦 - -isysroot:指定所编译的 sdk,这里我们使用模拟器的 sdk
- -I $lib_path:指定 include 的搜索路径
获取 umbrella header
在 ObjC 中引用代码是需要通过 #import
来完成的,而对于 ObjC 的 framework 而言,我们可以通过引入 umbrella header
来引入该 framework 暴露出来的全部 public header 文件。因此,可以理解为 umbrella header
是 ObjC framework 的 master header。具体可以看:讨论。
这一点需要感谢 Pod,它为我们的依赖库统一生成了 A-umberlla.h
文件,存放在 Target Support Files/A/A-umberlla.h
。
在此之前很多依赖库的 umbrella header
并不是很规范。经常会有一些文件是 public 状态,却未添加到 umbrella header
中,导致无法直接通过 umbrella header
来完成引用。包括很多公司维护的私有库也会经常忘记更新 umbrella header
的情况,好在 Pod 帮我们自动生成了。
复制 umbrella header
细心的同学从 AFNetworking 的文档生成命令中能发现,AFNetworking-umbrella.h
的位置是在源码的文件夹下。如果直接指定为 Target Support Files
下的 umbrella header 文件是无法生成文档的。我们需要把它复制到源代码在同层目录下。
那么问题来了:如何正确的获取源码所在目录。
首先想到的是和通过 .podspec
文件就能准确拿到 Source 目录。不过比较难实现,我们只能拿到的是 Local Podspecs
下的 .podspec
文件,否则需要在 pod install
时才能获取到。但是这么做需要修改 Podfile 也比较麻烦。
选择简单粗暴的方式,直接列出可能出现的 Source 路径:
1
2
3
4
5
6
7
8
9
10
11
12
# /A/Classes/...
# /A/src/a/...
# /A/A/Classe/...
# /A/A/Classes/...
# /A/A/Source/..
# /A/A/Sources/..
# /A/Source/A/...
# /A/Sources/A/...
# /A/Source/...
# /A/A/..
# /A/...
# libextobjc/extobjc
有用 Classes
、 Source
、Sources
、src
等等,情况五花八门,逐一匹配就可以了。
这么做是可以覆盖大部分的情况,但是仍然发现部分私有库生成的文档缺失甚至是空的。最终发现的问题是:clang 没有递归处理多级目录的文件,这里应该是参数没有正确设置,查看了 Clang 手册 感觉就是 -I
参数,不过也没有生效,有了解的同学求指点。
咋办,先暴力解决:
1
2
3
4
find $lib_path -type f ! -regex '*.\(h\|m\|swift\)' \
! -name '*.json' \
! -name '*.pdf' \
-exec mv -i {} $lib_path \;
将子目录下文件全部移到 framework 源码目录下,再通过 Jazzy 来生成文档,算是暂时解决问题了。
然而 AFNetworking 的文档依旧不是完整的,不过属于另外一种情况。目录如下,大家可以 🤔 一下:
Swift & ObjC 混编依赖库的文档生成
对 Swift & ObjC 混编的依赖库本身是不提倡的,虽然在实际开发过程中无法避免。
为了测试混编库的文档生成,这里新建一个 Pod 库:Mixin,添加了 MixinSwift 和 MixinObjC 两个类:
MixinSwift
1
2
3
4
5
6
7
8
9
10
11
/// Test Swfit Class import Objective-C's property
public class MixinSwift: NSObject {
/// say hello from Swift
@objc public static let sayHi: String = "Hi, I'm from Swift"
/// call Objective-C say Hi
@objc public class func callObjC() {
print("hello from MixinObjc: \(MixinObjC.sayHi)")
}
}
MixinObjC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import "MixinObjC.h"
#import <Mixin/Mixin-Swift.h>
@implementation MixinObjC
+ (NSString *)sayHi
{
return @"Hi, I'm from Objective-C";
}
+ (void)callSwift
{
NSLog(@"hello from MixinSwift: %@", MixinSwift.sayHi);
}
@end
由于 Jazzy 无法直接生成混编项目的文档,这里需要通过 SourceKitten
分别将 Swift 和 ObjC 的代码注释转成 json 的中间格式,才能生存完整的文档。生成命令如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lib_name=Mixin
output="public/docs/$lib_name"
swift_doc="$output/$lib_name-swift-doc.json"
objc_doc="$output/$lib_name-objc-doc.json"
lib_path=$(pwd)/Pods/$lib_name/$lib_name/Classes
umbrella_header="$lib_path/$lib_name-umbrella.h"
sdk_path=`xcrun --show-sdk-path --sdk iphonesimulator`
sourcekitten doc --objc $umbrella_header \
-- -x objective-c -isysroot $sdk_path \
-I $lib_path \
-fmodules > $objc_doc
sourcekitten doc -- -project Pods/Pods.xcodeproj -target Mixin > $swift_doc
jazzy -o $output --sourcekitten-sourcefile $swift_doc,$objc_doc
文档如下:
依赖库类型判断
由于不同类型的依赖库,其生成文档的脚本有所不同,我们还需要判断每个依赖库类型,是纯 ObjC、纯 Swift 还是混编类型。解决方式就是对 Source 目录下的文件类型进行 count 以判断依赖库类型:
1
2
3
4
5
6
7
8
9
10
11
12
swift_count=`find $lib_path -maxdepth 6 -type f -name '*.swift' | wc -l`
objc_count=`find $lib_path -maxdepth 6 -type f -name '*.m' | wc -l`
# file state, 0: only objc, 1: only swift, 2: swift & objc
lib_state=0
if [[ $swift_count -ge 1 && $objc_count -ge 1 ]]; then
lib_state=2
elif [[ $swift_count -eq 0 && $objc_count -ge 1 ]]; then
lib_state=0
elif [[ $swift_count -ge 1 && $objc_count -eq 0 ]]; then
lib_state=1
fi
静态文档的部署
我们使用是 Github Page 来进行文档部署,特别简单仅需在 repo 的设置页指定文档类型就可以了。剩下的就是提交代码,Git 会自动触发编译。
更多介绍请查看 Github Page 说明。
最后,完整 Demo 的托管地址为:Cocoa-Documentation-Example。
Git page 文档地址:https://looseyi.github.io/Cocoa-Documentation-Example,这个地址是 Github 自动生成的。效果如下:
One More Thing …
尽管我们当前的方案可以正确的生成文档,但是其实还可以更进一步。
当前的文档生成是基于 project
的方式,而我们完全可以针对每一个文件生成一份 json 数据,最后在把它们全部粘一起。命令的话 SourceKitten 都准备好了:
Swift 文件解析
1
$ sourcekitten doc --single-file $input_file -- -j4 $input_file >> $temp_outout
ObjC .h 文件解析
1
2
3
4
5
$ sourcekitten doc --objc \
--single-file $input_file \
-- -x objective-c \
-isysroot $sdk_path \
-I $lib_path -fmodules >> $temp_outout
通过这种方式,既不不需要配置 project
判断依赖库类型,也省去了查找找 umbrella header 的麻烦。
完整脚本传送门:docs_deploy.sh
- 多依赖库的文档生成还是比较简单的,感觉最难的还是读懂 Jazzy + SourceKitten 的文档和参数的配置。
- 思路是充分利用了 CocoaPods 为我们搭好的环境,在其之上就可以轻松生成文档,主题可定制哦。
- 倒腾过个人博客的同学,对于 Github Page 和文档的部署应该很熟悉,免费的 Github 资源还是要充分利用的。
知识点问题梳理
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:
Jazzy
对 API 的控制范围有几种选择?- 对于文中所采用的判断依赖库语言类型的方法是什么,还有更好的方式吗?
- ObjC 的
umbrella header
是从哪里获取的? - 扩展:
SourceKitten
所生成的 JSON 结构包括哪些字段?
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK