iOS 一步步带你实践组件二进制方案
<h2 id="前言">前言</h2><p>随着业务的扩展、项目体积的增大,<code>CocoaPods</code>组件库越来越多,每次重新编译的时候速度越来越慢,这给我们提出了需要提高编译速度的需求。</p>
<p>为了提高项目编译速度,对于大量使用组件化开发的项目组而言,组件二进制化是必然要走的路线,虽然中心思想就是要将各个组件打包成<code>.a</code>二进制库,但是各个公司可能方案都不太相同,网上的方案也有很多可供选择,这里我大体总结成以下几种:</p>
<ul>
<li>分仓库管理</li>
<li><code>Carthage</code>管理</li>
<li><code>podspec</code>环境变量(宏管理)</li>
<li><code>podspec</code>分<code>tag</code>管理(只针对私有库)</li>
</ul>
<p>前两个就不在这里讨论了可以看看这篇讲解。今天重点给大家分享一下第三和第四种方案的实施,但是目前只能针对私有库实施,对于一些第三方的公有库目前没有什么好的方案(😁 有好方法的同学可以在评论区推荐一下)。</p>
<h2 id="实施">实施</h2>
<h2 id="1创建pod私有库">1、创建pod私有库</h2>
<p>😝 如果您对这一块很了解请跳过这一步直接看第二步</p>
<p>对于私有库的创建,一般我们会采用<code>pod lib create XXX</code>模板来进行构建(如果还不知道这条命令是干嘛的同学可以先移步了解一下理解CocoaPods的Pod Lib Create)</p>
<p>这里我们拿<code>ABC</code>这个项目进行举例,首先我们执行<code>pod lib create ABC</code>创建<code>ABC</code>的私有库 <code>CocoaPods</code>会从<code>https://github.com/CocoaPods/pod-template.git</code>下载模板文件,并询问你一些构建信息,正常填就好了。</p>
<blockquote>
<p><strong>作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!</strong></p>
</blockquote>
<h4 id="以下资料在群文件可自行下载">以下资料在群文件可自行下载!</h4>
<p><img src="https://upload-images.jianshu.io/upload_images/12311242-212768070e142faf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<h4 id="推荐阅读">推荐阅读</h4>
<h4 id="ios开发最新-bat面试题合集持续更新中">iOS开发——最新 BAT面试题合集(持续更新中)</h4>
<pre><code>[MichaeldeMacBook-Pro:~ michaelwu$ pod lib create ABC
Cloning `https://github.com/CocoaPods/pod-template.git` into `ABC`.
Configuring ABC template.
------------------------------
To get you started we need to ask a few questions, this should only take a minute.
If this is your first time we recommend running through with the guide:
- https://guides.cocoapods.org/making/using-pod-lib-create.html
( hold cmd and double click links to open in a browser. )
What platform do you want to use?? [ iOS / macOS ]
>
复制代码
</code></pre>
<p>一般如果我们构建好了的话工程目录会类似这样一个结构:</p>
<pre><code>.
├── ABC
│ ├── Assets
│ └── Classes
├── ABC.podspec
├── Example
│ ├── ABC
│ ├── ABC.xcodeproj
│ ├── ABC.xcworkspace
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Pods
│ └── Tests
├── LICENSE
├── README.md
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj
复制代码
</code></pre>
<p>这里你会发现,<code>CocoaPods</code>已经帮我们创建好了<code>Demo</code>、源文件目录、<code>Podfile</code>、<code>podspec</code>、<code>.gitignore</code>文件等(真是一个贴心的小家伙),而且很规范,<code>Demo</code>文件在<code>Example</code>目录下</p>
<p>窥视一下<code>podspec</code>文件你就明白了源码需要指定在<code>./Classes/**/*</code>路径下</p>
<pre><code> s.source_files = 'ABC/Classes/**/*'
复制代码
</code></pre>
<p>为了演示效果,我们创建两个源文件<code>ABC.h</code>与<code>ABC.m</code>并放入<code>Classes</code>路径下,同时将默认的<code>ReplaceMe.m</code>删除</p>
<p>[图片上传中...(image-b08f0c-1594108050326-4)]</p>
<figcaption></figcaption>
<p>接着在<code>Example</code>下执行<code>pod install</code>,可以发现<code>ABC.h/m</code>已经导入成功</p>
<p>[图片上传中...(image-e75b0-1594108050326-3)]</p>
<figcaption></figcaption>
<p>至此,我们就明白了私有库的创建过程,需要编写源代码需要放入指定目录下并在执行<code>pod install</code>进行同步</p>
<h2 id="2创建静态库">2、创建静态库</h2>
<p>组件二进制其实指的就是打包成<strong>动态库/静态库</strong>,由于过多的动态库会导致启动速度减慢得不偿失,此外<code>iOS</code>对于动态库的表现形式只有<code>framework</code>,若想做源码与二进制切换时,引入头文件的地方也不得不进行更改,例如:</p>
<pre><code>import <ABC.h> // 源码引用
import <ABCBinary/ABC.h> // 动态库引用
复制代码
</code></pre>
<p>而打包成静态库<code>.a</code>文件(注意不要打包成<code>framework</code>形式)则不需要更改引用代码,所以综上所述,我们选择打包成静态库的方式不需修改引用代码、缩小体积提升编译速度。</p>
<p>确定目标之后,就是实施了,一般而言我们私有库都会在远程托管地址有<code>git</code>仓库,然后再上传到指定的私有源(specs)上,那么就会引申出几个问题:</p>
<ul>
<li>要不要将静态库上传到<code>git</code>(如果包体积很大会很占用<code>git</code>空间)</li>
<li>怎么做到一套代码同时管理源码和二进制</li>
<li>为了能够调试源码,如何在源码及二进制间切换(下一步骤会讲到)</li>
</ul>
<p>针对这几个问题,一一回答:</p>
<h2 id="3静态库与源码如何用同一套代码管理">3、静态库与源码如何用同一套代码管理?</h2>
<p>其实这个很简单,我们接着拿<code>ABC</code>这个项目举例子,进入<code>Example</code>打开我们的<code>ABC.xcworkspace</code>工程,然后创建新的<code>Target</code>为静态库,并取名为<code>ABCBinary</code>(一定要取这个名字,后面我会解释)</p>
<pre><code>File->New->Target->Static Library
复制代码
</code></pre>
<p>此时在<code>Example</code>目录下会增加刚刚创建的<code>Target</code>文件夹,结构如下:</p>
<pre><code>├── ABCBinary
│ ├── ABCBinary.h
│ └── ABCBinary.m
复制代码
</code></pre>
<p>Xcode默认会帮我们生成两个文件,我们将<code>.h</code>改名为<code>placeholder.h</code>,<code>.m</code>删除,这里为什么要将<code>.h</code>换成<code>placeholder.h</code>呢?先卖个关子,待会我们再作解释。</p>
<p>我们把刚才写的<code>ABC.h/m</code>的源码拖到<code>ABCBinary</code>中,注意不要勾选<code>Copy items if needed</code>,只做引用即可</p>
<p>[图片上传中...(image-d11b04-1594108050325-2)]</p>
<figcaption></figcaption>
<p>[图片上传中...(image-a6b089-1594108050324-1)]</p>
<figcaption></figcaption>
<p>之后我们需要到<code>ABCBinary</code>的<code>Build Setting</code>中指定静态库所能运行的最低版本:</p>
<pre><code>Build Setting->Deployment->iOS Deployment Target
复制代码
</code></pre>
<p>并在<code>Build Phases</code>中指定头文件,将<code>ABC.h</code>拖入Public中,具体步骤:</p>
<pre><code>TARGETS->ABCBinary->Build Phases->New Header Phase
复制代码
</code></pre>
<p>至此我们完成了一套代码管理二进制与源码,但有个小细节需要注意:就是如果源代码有变动需要在<code>XXXBinary</code>文件中重新导入一遍,不然二进制的文件不会自动更新(同学们有好的建议可以评论区讨论下)</p>
<h2 id="4是否需要将二进制上传至git">4、是否需要将二进制上传至git?</h2>
<p>其实<code>git</code>对代码管理时会将不同的<code>diff</code>做备份(在<code>.git</code>这个文件夹下),但是对于二进制文件来说<code>git</code>就没用那么友好了,会将二进制的每一次提交都做磁盘备份,以便于随时版本回滚,倘若我们每次都对私有库进行更新时都将二进制包传至<code>git</code>,那么时间久了无疑是对<code>git</code>仓库空间的一个挑战(如果你们公司空间足够大不需要考虑,那么请忽略这一步)</p>
<p>网上有很多针对这个问题给出的解决方案,但都不是很完美,大体上都是说将<code>二进制包</code>单独传到另一份静态资源地址,以此解决<code>git</code>过大问题,不过我觉得没有解决痛点,能不能不上传二进制包呢?</p>
<p>结论当然是可以,<code>CocoaPods</code>本地的缓存目录在</p>
<pre><code>~/Library/Caches/Cocoapods
复制代码
</code></pre>
<p>其实每次我们更新<code>pod</code>库时,<code>CocoaPods</code>都会先从指定源去拉源代码再根据该库的<code>podspec</code>文件指定输出目标文件,那么我们如果能把静态库打包推迟到<code>pod install</code>阶段就不需要上传二进制包到<code>git</code>了,但是如何做到延迟打包呢?</p>
<p>很幸运,<code>CocoaPods</code>提供了针对<code>podspec</code>的预执行脚本,prepare_command(戳我进官网)命令,该命令可以指定相应的脚本在<code>pod install</code>时去执行,那么我们就可以将编译打包的脚本放入其中,从而完成<strong>延迟打包</strong></p>
<p>好了,理论上貌似可行了,实践出真知啊(😄 绝对不能做一个理论性选手啊),具体怎么做?</p>
<p>首先我们需要一个能一键打静态库包的脚本(一刀99级那种),帅气的我这边已经为大家准备好了,只修改一下<code>PROJECT_NAME</code>即可,拷贝脚本至根目录并赋予执行权限:</p>
<pre><code># 当前项目名字,需要修改!
PROJECT_NAME='ABC'
# 编译工程
BINARY_NAME="${PROJECT_NAME}Binary"
cd Example
INSTALL_DIR=$PWD/../Pod/Products
rm -fr "${INSTALL_DIR}"
mkdir $INSTALL_DIR
WRK_DIR=build
BUILD_PATH=${WRK_DIR}
DEVICE_INCLUDE_DIR=${BUILD_PATH}/Release-iphoneos/usr/local/include
DEVICE_DIR=${BUILD_PATH}/Release-iphoneos/lib${BINARY_NAME}.a
SIMULATOR_DIR=${BUILD_PATH}/Release-iphonesimulator/lib${BINARY_NAME}.a
RE_OS="Release-iphoneos"
RE_SIMULATOR="Release-iphonesimulator"
xcodebuild -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphoneos clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_OS}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_OS}"
xcodebuild ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphonesimulator clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_SIMULATOR}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_SIMULATOR}"
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
cp -rp "${DEVICE_INCLUDE_DIR}" "${INSTALL_DIR}/"
INSTALL_LIB_DIR=${INSTALL_DIR}/lib
mkdir -p "${INSTALL_LIB_DIR}"
lipo -create "${DEVICE_DIR}" "${SIMULATOR_DIR}" -output "${INSTALL_LIB_DIR}/lib${PROJECT_NAME}.a"
rm -r "${WRK_DIR}"
复制代码
</code></pre>
<p>我们还是拿<code>ABC</code>的项目来接着实践,拷贝脚本后,先来看一下我们<code>ABC</code>目前的结构:</p>
<pre><code>.
├── ABC
│ ├── Assets
│ └── Classes
├── ABC.podspec
├── Example
│ ├── ABC
│ ├── ABC.xcodeproj
│ ├── ABC.xcworkspace
│ ├── ABCBinary
│ │ └── placeholder.h
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Pods
│ └── Tests
├── LICENSE
├── README.md
├── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj
└── build_lib.sh
复制代码
</code></pre>
<p>可以看到最下面多了一个<code>build_lib.sh</code>脚本(就是刚刚拷贝的那个脚本),另外<code>ABCBinary</code>里面有一个<code>placeholder.h</code>,这里解释一下之前埋下的悬念:因为<code>ABCBinary</code>文件夹里对于源码的引用没有<code>copy</code>,所以在提交到<code>git</code>时会自动将文件夹清空(也就是说在git目录里找不到),因此需要加一个占位防止文件夹不上传到<code>git</code>,但是切记不要编译到静态库里!</p>
<p>好的,至此一键打包脚本也准备好了,通过查看脚本我们发现这个二进制包最终会输出到根目录下的<code>./Pod/Products/</code>目录中,那不还是得传到<code>git</code>吗?别急,你忘了<code>gitignore</code>了吗?</p>
<p>配置<code>.gitignore</code>忽略<code>Pod/</code>文件不就行了嘛,在<code>.gitignore</code>最下面增加忽略</p>
<pre><code>Pod/
复制代码
</code></pre>
<p>好了至此,我们完成了自动打包脚本及<code>git</code>忽略二进制包,再也不用担心我们的<code>git</code>仓库空间压力了(运维小哥哥们表示“尼玛松了一口气”)</p>
<h2 id="5如何在源码与二进制间切换">5、如何在源码与二进制间切换</h2>
<p>在提升编译速度的前提下,还需要考虑到能随时进行源码调试,这就涉及到了如何在源码与二进制间切换的问题,网上的思路有很多:环境变量、白名单、tag切换等。</p>
<p>这几种方式在<strong>前言</strong>部分我们已经讲过了,接下来我们介绍一下“环境变量”和“tag切换”这两种方式:</p>
<h3 id="51-如何利用tag进行切换">5.1、 如何利用tag进行切换:</h3>
<p>首先我们需要约定好规则:当<code>version</code>中包含<code>.Binary</code>关键字时执行<code>prepare_command</code>命令并输出<code>source</code>为静态库,具体操作如下(<code>podspec</code>是用<code>ruby</code>写的,支持条件判断):</p>
<pre><code>if s.version.to_s.include?'Binary'
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is binary now'
puts '-------------------------------------------------------------------'
s.prepare_command = '/bin/bash build_lib.sh'
s.source_files = 'Pod/Products/include/**'
s.ios.vendored_libraries = 'Pod/Products/lib/*.a'
s.public_header_files = 'Pod/Products/include/*.h'
else
s.source_files = 'ABC/Classes/**/*'
end
复制代码
</code></pre>
<p>由于<code>tag</code>是根据<code>version</code>走的(<code>tag => s.version.to_s</code>),因此只需要我们修改<code>s.version = '0.1.0.Binary'</code>即可实现二进制打包</p>
<p>好,我们贴一段此时<code>ABC.podspec</code>完整的代码:</p>
<pre><code>Pod::Spec.new do |s|
s.name = 'ABC'
s.version = '0.1.0.Binary'
s.summary = 'A short description of ABC.'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/609223770@qq.com/ABC'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '609223770@qq.com' => '609223770@qq.com' }
s.source = { :git => 'https://github.com/609223770@qq.com/ABC.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
if s.version.to_s.include?'Binary'
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is binary now'
puts '-------------------------------------------------------------------'
s.prepare_command = '/bin/bash build_lib.sh'
s.source_files = 'Pod/Products/include/**'
s.ios.vendored_libraries = 'Pod/Products/lib/*.a'
s.public_header_files = 'Pod/Products/include/*.h'
else
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is source code now'
puts '-------------------------------------------------------------------'
s.source_files = 'ABC/Classes/**/*'
end
end
复制代码
</code></pre>
<p>让我们来看看效果,在<code>Example</code>下执行<code>pod install</code>,发现切换过来了,Nice 😝~</p>
<p>[图片上传中...(image-84413-1594108050322-0)]</p>
<figcaption></figcaption>
<p>接下来验证本地<code>podspec</code>(若有问题按照提示更改,<code>ssh://xxx.git</code>是你私有源的地址):</p>
<pre><code>pod lib lint --sources=ssh://xxx.git --allow-warnings --verbose --use-libraries
复制代码
</code></pre>
<p>若没问题,在<code>ABC</code>的<code>git</code>仓库打一个<code>0.1.0</code>的版本<code>tag</code>,并上传<code>ABC.podspec</code>至私有源,上传成功后修改<code>podspec.version</code>为<code>0.1.0.Binary</code>再次执行上传:</p>
<pre><code>pod repo push XXXSpecs ABC.podspec --allow-warnings --verbose --use-libraries
复制代码
</code></pre>
<p>✅ 如果一切顺利,我们已经将Binary和源码的<code>ABC</code>上传到了私有源。</p>
<p>接下来我们在实际项目实验一下,<code>Podfile</code>中指定,并执行安装</p>
<pre><code>pod 'ABC', '~> 0.1.0' # source code
pod install
复制代码
</code></pre>
<p>不出意外源码<code>ABC</code>安装成功,这时我们修改<code>tag</code>版本后面加<code>.Binary</code>,再次执行<code>pod install</code>,如下所示:</p>
<pre><code>pod 'ABC', '~> 0.1.0.Binary' # source code
pod install
复制代码
</code></pre>
<p>很遗憾,你可能会发现源码并没有切换成功,为什么呢?</p>
<p>原来<code>Pod</code>的版本管理是放在<code>Podfile.lock</code>中,每次执行<code>pod install</code>时若<code>Podfile.lock</code>中已经存在此库,则只下载<code>Podfile.lock</code>文件中指定的版本进行安装,否则去搜索这个<code>pod</code>库在<code>Podfile</code>文件中指定的版本来安装。</p>
<p>因此,解决办法有两种,一种是从<code>Podfile.lock</code>中将包含<code>ABC</code>的地方全部删除或是干脆直接删除<code>Podfile.lock</code>,再次执行<code>pod install</code>会发现切换变过来了。</p>
<p>还有一种方法是执行<code>pod update</code>,这也是 update 和 install 的区别,update会读取<code>Podfile</code>中的版本去更新<code>Podfile.lock</code>文件。(戳我查看pod install和pod update区别)</p>
<pre><code>pod update ABC
复制代码
</code></pre>
<p>执行后,先是会更新一下master和其他私有源,再去更新<code>ABC</code>,发现此时切换成功。(缺点就是如果<code>Podfile</code>中如果某些库没有指定版本就会更新到最新版本)</p>
<h3 id="52如何利用ruby环境变量进行切换">5.2、如何利用Ruby环境变量进行切换:</h3>
<p>Ruby语法支持一些环境变量的读取,因此可以在<code>pod install</code>时增加参数以此判断是否要切换源码:</p>
<pre><code>IS_BINARY=1 pod install # 1 代表二进制
IS_BINARY=0 pod install # 0 代表源码
pod install # 默认也是0 源码
复制代码
</code></pre>
<p>在<code>podspec</code>中做修改:</p>
<pre><code>Pod::Spec.new do |s|
s.name = 'ABC'
s.version = '0.1.0.Binary'
s.summary = 'A short description of ABC.'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/609223770@qq.com/ABC'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '609223770@qq.com' => '609223770@qq.com' }
s.source = { :git => 'https://github.com/609223770@qq.com/ABC.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
if s.version.to_s.include?'Binary' or ENV['IS_BINARY']
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is binary now'
puts '-------------------------------------------------------------------'
s.prepare_command = '/bin/bash build_lib.sh'
s.source_files = 'Pod/Products/include/**'
s.ios.vendored_libraries = 'Pod/Products/lib/*.a'
s.public_header_files = 'Pod/Products/include/*.h'
else
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is source code now'
puts '-------------------------------------------------------------------'
s.source_files = 'ABC/Classes/**/*'
end
end
复制代码
</code></pre>
<p>同tag切换一样,这种方式在实际项目中切换也存在问题,需要两个必要步骤:</p>
<pre><code>pod cache clean ABC # 先清理ABC的pod缓存
rm Pods/ABC # 再把ABC从实际项目中的Pods目录下移除
复制代码
</code></pre>
<h2 id="6对比两种方式">6、对比两种方式</h2>
<table>
<thead>
<tr>
<th>方式</th>
<th>优点</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ruby环境变量切换</td>
<td>1、不需要上传两份podspec</td>
<td></td>
</tr>
<tr>
<td>2、切换时不需要修改Podfile</td>
<td>1、需要清除私有库的缓存</td>
<td></td>
</tr>
<tr>
<td>2、需要手动删除/Pods/XXX</td>
<td></td>
<td></td>
</tr>
<tr>
<td>3、不能针对单独库进行切换,除非自定义白名单之类的规则</td>
<td></td>
<td></td>
</tr>
<tr>
<td>tag切换</td>
<td>1、可以针对单独某个库进行切换</td>
<td>1、需要执行pod update(需等待repo master源的更新)</td>
</tr>
<tr>
<td>2、私有库的tag需要打两个,podspec上传时需要传两次</td>
<td></td>
<td></td>
</tr>
<tr>
<td>3、切换时需要手动修改Podfile文件的版本信息</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<h2 id="7总结">7、总结</h2>
<p>好,至此iOS组件二进制方案就介绍完了,我们通过<code>ABC</code>项目的实践了解了整个过程:</p>
<ul>
<li>创建pod私有库</li>
<li>在私有库Demo中创建静态库target,并配置头文件及最低iOS版本支持</li>
<li>创建打包脚本</li>
<li>设置<code>.gitignore</code>忽略输出的二进制包</li>
<li>配置podspec根据tag版本判断或根据环境变量判断</li>
<li>验证并上传源码及二进制的podspec</li>
<li>在实际项目中切换时需要执行<code>pod update</code>或删除<code>Podfile.lock</code>中相关库信息</li>
</ul>
<h2 id="8链接">8、链接</h2>
<p>本文demo相关链接如下,另附自动上传podspec脚本地址(相关文章),喜欢的朋友点个star</p>
<ul>
<li>组件化方案demo地址:CocoaPodsBinary</li>
<li>自动上传podspec脚本:upload_podspec</li>
</ul>
<blockquote>
<p><strong>作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!</strong></p>
</blockquote>
<h4 id="推荐阅读-1">推荐阅读</h4>
<h4 id="ios开发最新-bat面试题合集持续更新中-1">iOS开发——最新 BAT面试题合集(持续更新中)</h4>
<p>作者:被帅醒的吴宝宝<br>
链接:https://juejin.im/post/5efaf0655188252e42157e8a</p><br><br>
来源:https://www.cnblogs.com/iOSer1122/p/13269702.html
頁:
[1]