一、背景:

Q音直播抽离成pod库分别引入到QQ音乐和Fan直播两个独立app中,而对于直播业务来讲,直播SDK通过pod本地引入集成到Demo中进行日常直播业务的开发,通过Demo来精简工程规模,提高研发效率。

但随着业务扩展直播SDK越来越庞大,出现了以下痛点:

  1. 以快速开发为目标的直播Demo工程编译时间越来越久,影响组内同事的开发效率;

  2. 直播SDK最开始以源码方式接入宿主,增加了约800s的编译时长,影响宿主开发效率;

  3. SDK引入宿主流程繁琐、CI效率低,导致测试及灰度阶段出包验证问题耗时严重。

于是决定逐一解决以上三个痛点。

二、预研:

A. 背景:

直播Demo通过本地pod引入直播SDK去日常开发,每次出现文件配置变更时需要重新执行pod;频繁pod常会导致编译缓存失效,引起整个pod库的重新编译。

对于痛点1:优化编译速度,有很多方式去做。但最为有效的措施包含以下两点:

  • 网络请求使用jce协议,开发至今jce文件量已经很大(2000+),但实际不必暴露实现,因此可二进制引入。

  • 直播工程依赖的外部pod库可以二进制引入。

对于痛点2:直播SDK日常开发调试是在独立工程中进行,无需对Q音主端暴露源码,因此可以将整个直播SDK以静态库的形式引入到主端。

针对以上解决方案的设想,选择合理的二进制方案至关重要。

B. 二进制方案预研:

a. 工程脚手架+打包脚本

这是常规的打包方式,我们可以选择不同的XCode工程模版来打包静态库(.a | .framework)或动态库(.frame)。主要分以下几步:

  1. 创建XCode模版工程,并配置好二进制包支持的架构等参数。

  2. 执行 pod install/update 将需要的pod库引入。

  3. 选择需要暴露的头文件。

  4. 打包支持模拟器架构的静态库( Build Active Architecture Only=NO 可支持所有模拟器架构)

  5. 打包支持真机架构的静态库。

  6. 合并生成的静态库。

步骤1是要提前搭好的工程脚手架,后面的步骤可以编写打包脚本来简化操作。

b. cocoapods-packager

cocoapods-packager是cocoapods官方的一款二进制打包插件,通过gem安装后可通过 pod package 命令行来生成 framework 或 static library。使用非常方便,只需要提供一个podspec文件即可完成打包。

看了一下插件的源码,实现逻辑倒也不复杂,关键步骤如下:

1. 将提供的podspec迁移到一个沙盒目录下,根据此podspec生成podfile文件。

2. 执行 pod install 生成pod工程(podfile中需要设置配置项intefrate_targets为false,不然会因找不到target而报错)。

3. xcodebuild 生成二进制包,然后合并模拟器及真机并输出到指定路径。

c. cocoapods-binary

如果说cocoapods-packager仅仅是针对单个pod库的打包,那么cocoapod-binary则是对工程中整个pod库的二进制方案。它在 pod install 时通过将引入的pod库预编译成binary然后缓存至本地,后续工程编译直接link到binary,对于binary的pod库以几乎零编译成本的形式来提高整个项目的编译效率。

同时cocoapods-binary可以通过修改podfile灵活地切换源码和二进制,优化编译效率的同时也方便调试。

C. 总结:

方案 优点 缺点 适用场景
工程脚手架+打包脚本 完全自主打包,可任意修改工程配置支持不同打包方式,灵活性更强。 需要自行维护脚手架仓库。 原则上来讲只要知识到位所有场景都可用,不过存在一定的学习和维护成本。
cocoapods-packager 只需要提供一个podspec便可零成本打包,使用方便,学习成本低。 提供的打包参数有限,如有额外需求需要自行修改插件。 适用于对单个pod仓库进行打包。
cocoapods-binary

插件轻量对podfile原有语法无入侵,不需要额外的学习和操作成本;

二进制流程完全自动化;

可以方便地进行源码->二进制切换。

插件的部分功能还不太完善。如某个模块更新以后,需要 pod update 才能保证二进制也得到更新。 不同于packager作用于单个pod,项目的pod仓库可能会频繁变更版本,不可能每次版本变更都去对应打静态库,所以对于整个工程的pod库是一个不错的二进制选择。

三、方案落地:

A. 痛点一:Q音直播编译优化

以下编译时间皆以我的17款iMac(i7|16G)上的iphone11模拟器来计算。

a. 编译选项优化

1. Debug下设置Build active architecture only 为 YES,debug配置下没必要生成全架构。

2. 设置Debug下不生成dSYM,只在release下生成。

3. Build Setting -> Header Search Paths->non-recursive

如果过多头文件索引被设置为了递归引用,会导致编译器预处理头文件效率变低。

4. 关闭 Enable Index-While-Building Functionality

实践中,1和2 XCode12默认已经开启;3跟4减少的时间可忽略不计,所以我们还要另寻出路。

b. jce二进制集成

直播SDK内采用jce协议,开发至今模块内的jce文件数量>2000,大大增加了编译时长。

jce文件只依赖cocoaJce一个pod库且无外部资源引入,选择打包成.a静态库。由于业务迭代需要jce变更非常频繁而且存在多个版本并行迭代同时变更jce的情况,因此我们要做好版本管理,同时希望将打包流程自动化。

1. 将jce_oc文件通过pod本地引入(不需要手动链接文件),将pod操作+打包流程写为自动化脚本。

2. 将打包流程及头文件的导出分离,工程及打包脚本只负责打包,专写一个脚本负责从源文件按目录结构导出头文件放在Header下。(传统的方式是要在XCode工程中手动选择暴露的Header)。

3. 规范目录层级

  • 之前 jce 是按照功能模块划分文件夹的;改为pod引入后是不会划分目录的,导致所有文件暴露在一个文件夹下,增加了大家的查找成本。

  • pod规范目录层级有两种方式:1. 本地pod引入;2. 规范subspec。

  • 这里因为需要做pod发布选择了第二种方式,由于目录层级在每次编译jce文件时都可能会变更,因此将subspec格式规范好每次通过脚本写入到subspec.rb文件中,并将此rb文件绑定到podspec中。

f = open('subspec.rb', 'w+')for entry in os.listdir(basepath):   if os.path.isdir(os.path.join(basepath, entry)):       str = f"s.subspec '{entry}' do |ss|\n"       str = str + f"\tss.source_files = \"{basepath}/{entry}/**/*\"\n"       str = str + f"\tss.public_header_files = \"{basepath}/{entry}/**/*.h\"\n"       str = str + "end\n\n"       f.write(str)f.close()

4. 版本管理:

  • 同一版本的jce会频繁变更,因此写了个脚本去校验spec仓库中对应版本的jce.podspec是否存在。

  • 若不存在则拷贝根目录下的podspec备份,同时修改version及source(指向当前版本的jce二进制仓库的对应分支)。

  • 如果存在则只是更新podspec(主要是更新subspec确保目录层级正确)。

5. 自动化:

  • 将上述流程接入到之前的jce->oc编译的CI构建中,做到了jce编译+编译产物打包+pod集成发布流程的自动化。

效果:初次编译时间从400s减到了160s

c. 耗时模块二进制

使用Gonmon计算了一下单文件的编译时间。

# 安装xcpretty和gnomonsudo gem install xcprettynpm install -g gnomon# 获取文件编译时间并用xcpretty对输出结果进行格式化xcodebuild -workspace xxx.xcworkspace -configuration "Debug" -scheme "xxx" -destination "platform=iOS Simulator,id=xxx" | xcpretty | gnomon -i | grep Compiling >>fileTime.txt# 使用sort对输出结果进行排序sort -r fileTime.txt | head -n 100 | grep Compiling >> new_fileTime.txt

输出结果:

可以看出单个文件编译耗时比较久的很多是c++或oc/c++混编文件,果然引入c++静态库对iOS来讲就是编译灾难。高居首位的是KSIMSDK中的一个混编文件,其中大部分逻辑是拿c++写的。包括MMKV里面也有一些编译耗时比较久的文件。

像是KSIMSDK这种我们工程中用到的又不常变更的可以将其编成静态库引入,对于MMKV这种通过pod引入的则可通过pod-binary方案将编成framework(也就是下一步做的)。

效果:初次编译时间从160s减到了140s

d. cocoapod二进制
cocoapod-binary

pod-binary优化编译速度的原理在第二章节预研的时候讲过了,故这里只讲用法。

此方案要求pod库必须是以framework方式进行集成,即要开启use_framework!。可选配置如下:

配置 备注
all_binary! 加入后默认全部pod都编为二进制,可配合binary=>false/true来使用
keep_source_code_for_prebuilt_frameworks! 编译完成后保留源码
enable_bitcode_for_prebuilt_frameworks! 开启Bitcode
cocoapod-packing-cubes

pod默认的集成方案为static+library,开启了use_framework!后集成方案变为了dynamic+framework,非常不灵活,因此配合cocoapods-packing-cubes插件一起使用。

配置 备注
static+library 集成方式为.a静态库
static+framework 集成方式为.framework静态库
dynamic+framework 集成方式为.framework动态库
安装:
sudo gem install cocoapods-binarysudo gem install cocoapods-packing-cubes
使用方式:

效果:初次编译时间从140s减到了90s

e. 优化头文件索引
(1). 用好pch文件

PCH文件是一个标准的预编译头文件( Pre-Compiled Header),其文件里的内容能被项目中的其他文件访问。

  • 优点是其他文件想引用写在pch的头文件不需要重复import;同时pch预编译完成后,其他引用到pch的文件编译速度也会加快。

  • 缺点是如果pch引用的头文件发生改变可能引发大规模重编。

我们把一些全局的宏定义放到pch内,由于直播模块是通过pod引入的,所以使用pch需要在podspec中相应去设置:

#podspecs.prefix_header_file = 'Classes/PrefixHeader.pch'
// pch#ifdef __OBJC__#import "MLLSLog.h"#import "MLiveSDKMacro.h"#endif
(2). 用好前向声明

llvm支持修改编译参数来查看编译各个阶段的耗时,包含-ftime-report与-ftime-trace。前者是打印出一堆格式化数据,而后者则是生成火焰图,相对来讲比较直观一些。

从火焰图中可以看出编译前端中对头文件的处理最为耗时,大概率是头文件的嵌套引用较为复杂。可以考虑优化topN的头文件引用。

减少头文件中无用类的引入,改为前向声明。可以使用IWYU(include-what-you-use)来做,它的主要功能是去分析头文件中的每个include是否必要,然后将不必要的引用替换掉从而提升编译速度。

这里通过yangyang大神提供的工具分析了一下头文件被引用次数及总处理时间,根据表格选取了我们工程中的top10的文件进行了头文件的引用优化。

由于直播模块只是优化了top10便效果很明显了,所以没有进一步用IWYU去处理。

PS:关于火焰图以及IWYU等工具的使用可以参考yangyang大神的文章,这里就不班门弄斧介绍了(https://cloud.tencent.com/developer/article/1564372)。

效果:初次编译时间从90s减到了40s

阶段总结:

经过以上的优化措施,直播工程的初次全量编译时间从最初的近400s减少到了现在的40s;同时少量修改后的增量编译时间只需要秒级,几乎达到了热重载的调试效果,较大程度地提升了组内同事们的研发效率。

附. 其他方案
ccache

在XCode9编译存在一个bug,pch会在无任何改动时触发重新编译,由此导致所有依赖pch的文件都会重新编译,产生预期外的全量编译。ccache主要是为解决此bug应运而生的方案,但随着XCode10解决了pch编译的bug后此方案便被废弃。

同时ccache会导致无缓存时首次编译时间几乎翻倍增加,故没有采用此方案。

distcc分布式编译

distcc的原理是把一部分需要编译的文件发送到服务器上,服务器编译完成后把编译产物传回来。但是分派任务的效率较低,分派+回传的过程耗费的时间经常会超过本地编译的时间,也没有采用。

B. 痛点二:直播模块二进制

直播SDK的二进制方案选择了cocoapods-packager进行打包。

a. pod package使用
pod package QMLiveCombineModule.podspec \   --exclude-deps \   --spec-sources=xxx,https://github.com/CocoaPods/Specs.git \   --no-mangle \   --force

安装后可直接通过以上命令来打包,提供的打包参数不逐一解释了,这里只解释两个比较难理解的参数:

  • --exclude-deps:不包含依赖的符号表,这里分两种情况使用:a).如果是静态库的话要使用此命令,否则外部引入被依赖库的话会报duplicatesymbol。b).如果是动态库的话则不要加此命令,动态库一定需要包含依赖的符号表。

  • --no-mangle:表示不使用name mangling技术,pod package默认是使用这个技术的。此命令会将我们的符号改为Pod#{pod_name}_#{symbol}这种格式。

如果我们使用了--exclude-deps命令,那么我们依赖的其他库则需要在主端引入,如果此时开启了name mangling则会导致打的包报错undefine symbol。所以这两条命令是配合使用的,打成包含其他依赖的静态库的时候一般会同时使用这两行命令。

pod package在打包时会为打包工程分配一个沙盒路径。因此将被打包的工程与podspec放在同一目录下,再通过source_files根据相对路径引入是不会生效的。它实际是会读取podspec中的source并去拉取远端代码到沙盒路径后再引入的。

因为之前直播SDK是通过pod拉取git branch引入主端的,所以只需将podspec中的source也改为拉branch,branch通过变量传入即可将打包流程脚本化。

b. pod工程配置

打静态库时将需要修改的工程配置写在podspecpod_target_xcconfig中。

  • 静态库中包含category,因此需要设置 'OTHER_LDFLAGS' => '-ObjC'

  • 打的静态库中模拟器希望不包含i386架构(减少包大小),因此需要设置 'VALID_ARCHS[sdk=iphonesimulator*]' => '$(ARCHS_STANDARD_64_BIT)'

  • 改用XCode12打包后会在lipo create阶段会报错 xxx have the same architectures (arm64) and can't be in the same fat output file,因此需要设置:

'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64' => 'arm64 arm64e armv7 armv7s armv6 armv8','EXCLUDED_ARCHS' => '$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT))',
c. 静态库pod集成

将pod源码打包成静态库后静态库本身再集成到pod引入到主工程中。

  • 集成在framework中的资源直接导入并不会生效,在静态库的podspec中写好资源引入。

  • 之前源码引入需要的依赖也要写在静态库引入的podspec中。(这里为了防止每次修改依赖多处的podspec都要跟随修改,将依赖项抽成一个ruby脚本在podspec中引入,每次修改依赖只需要改在公共的rb脚本即可)。

d. 头文件引入规范

改为静态库引入后之前主端引入多处报错,主要原因是之前通过"MLiveRoom.h"这种形式是不规范的,改为静态库后会导致工程索引不到,改为即可。

这里由于主端引入较多,逐一修改工作量较大,因此通过脚本来自动化此过程。思路是递归搜索直播SDK包含的头文件并记录下来存为数组Arr,再递归遍历主工程文件中引用了Arr中的行,然后规范为正确的格式。

e. 打包流程CI

将打包流程跑通后部署到蓝盾上做自动化。

  • macos构建机没安装pod package脚本,切换到腾讯的镜像源进行安装:

sudo gem install -n /usr/local/bin --source http://mirrors.tencent.com/rubygems/ cocoapods-packager -v 1.5.0

打XCode12的包需要命令行切换:

sudo xcode-select -s /Applications/Xcode_12.app/Contents/Developer/

打包执行pod会拉取cocoapod-spec,此操作耗时较久。

可通过切源或直接固化构建机ip来解决此问题。

阶段总结:

直播SDK静态库引入后,以Generic时间统计,Q音编译时长从>2000s减少到1000~1200s

C. 痛点三:优化Q音的出包速度

1. 问题背景:

之前如果在灰度期间,改bug并回归验证的步骤是:

  1. 在直播独立工程中修改完成验证通过,推到远端;

  2. 在Q音中开bug_fix分支,pod update QMLiveModule(指向直播独立工程对应的bug_fix分支)。等编译通过(由于最早直播模块是通过pod源码引入,执行完pod后工程配置大规模变更,几乎要执行一次全量编译,耗时久)。

  3. 再将编译通过的bug_fix推到远端,启动流水线编包。(编包流水约1h)。

整套流程下来至少1个半小时,尤其是在灰度以及发版前会阻塞所有相关负责人时间,太痛了!!

2. 可行性分析:

因此想专门固化一条蓝盾流水线来缓存编译产物,增量编译来提高出包速度。先说说方案的可行性:

  1. 解决了之前阐述的痛点一后直播模块以静态库的形式引入,每次pod后Q音主端的工程配置不会发生改变。这样每次更新直播的逻辑并不会导致编译缓存失效。

  2. 直播模块已经抽离得相对独立,但需要在Q音主端验证bug。不过不必时时更新Q音主端的逻辑,由此将编译产物失效的概率降为了0。

3. 方案实施:

既然增量编译的方案可行,接下来就可以编写脚本了。主要是以下几点:

  1. 打包前不要xcodebuild clean,这会清理缓存。

  2. 打包时选择build而不要归档,归档会忽略缓存。

  3. 将缓存存在一个固定路径下,每次打包时将derivedDataPath指向这个路径。

  4. 不同于归档,build后的产物是.app,我们要将其格式改为ipa。

  • 企业内测包格式非常简单,我们可以下载一个内测包解压看它的目录层级。为(Payload/QQMusic.app)

  • 将编译后的.app按照上述的目录层级放到Payload内,然后压缩成ipa格式。(zip -qry ${ipaFileName} Payload)。

将以上脚本部署到固化ip的流水上,增量编译后Q音的构建时间从之前的近50min减少到了4min30s

4. 进一步加速:

直播模块由于需要使用一些特性,所以限制了系统最低版本为iOS11,而11支持的最低机型是iphone 5s,这是第一部arm64机。同时固化流水出的包本来也只是给测试同学验证而不做上架,所以选择只编arm64架构的包。

做完这步后,打包时间又从4min30s减少到了3min30s

阶段总结:

通过增量编译使得真正的打包时间可以从>50min减少到3min,目前此固化ip的流水已经在灰度期间真正投入使用,流水线空闲期间同事们日常出包也开始使用此流水,后续考虑申请固化多几条流水供大家使用。

蓝盾上固化ip的流水不执行会定期回收,因此加了个定时触发的逻辑,半夜空闲时偷偷执行,同时判断是定时执行触发时主动去拉取Q音主端的变更,虽然不必要但由此也可保证每天至少同步一次Q音主端的逻辑。

四、总结:

对于编译优化来讲,通过实践得出的几点建议:

  • 做好模块化,对于变更不频繁或与自身业务不相关的模块通过pod/二进制方式集成。

  • 规范头文件的引用,合理使用pch文件。

    • 尽量减少在头文件中引入其他类,多使用前向声明。枚举的引入尽量跟类定义拆开。

    • 对于实现协议需要引入头文件可以将协议放到.m文件的类extension中,不必放在头文件类的声明处。

    • 不到万不得已头文件中不要引c++静态库。

  • oc/c++混编或纯c++文件编译编译耗时很大,酌情使用。

  • 工程配置类的耗时优化基本上有效的XCode都已默认开启,只需检查一下是否被关闭即可。还有一些可能会为项目带来不必要损失的工程配置优化,可根据项目需要酌情使用。

对于二进制方案来讲,没有真正意义的优劣之分,关键是使用场景。例如普通的工程打包用XCode脚手架+打包脚本即可应对;针对单个复杂一点的pod库打包可使用cocoapod-packager来打包;对于整个项目所有的pod的二进制方案则可选用cocoapod-binary,灵活起见可配合cocoapod-packaging-cubes来使用。

在探索过程中发现cocoapods还是有不少好用的插件的,同时也支持我们自定义插件;除了以上实际用到的再推荐一款cocoapods-open。

后续还要进一步做好项目的模块化,逐步做到只编我需要的部分

QQ音乐招聘Android/ios客户端开发,点击左下方“查看原文”投递简历~

也可将简历发送至邮箱:tmezp@tencent.com

supersu二进制更新安装失败_Q音直播编译优化与二进制集成方案相关推荐

  1. c++ 输出二进制_Q音直播编译优化与二进制集成方案

    一.背景: Q音直播抽离成pod库分别引入到QQ音乐和Fan直播两个独立app中,而对于直播业务来讲,直播SDK通过pod本地引入集成到Demo中进行日常直播业务的开发,通过Demo来精简工程规模,提 ...

  2. supersu二进制更新安装失败_helm安装教程

    Helm 帮助您管理 Kubernetes 应用程序--Helm Charts 帮助您定义.安装和升级最复杂的 Kubernetes 应用程序. 安装Helm 官方参考文档:https://helm. ...

  3. supersu二进制更新安装失败_vcpkg更新及产品路线图

    蝎子 vcpkg是一套跨平台,开源的C/C++库管理器,今天的这篇文章是有关vcpkg主题的2020年4月博文更新.在这篇文章中,我们将分享有关vcpkg 2020.04发布版本的一些信息以及vcpk ...

  4. 微软Win10 KB5012117更新安装失败 出现了“0x800f0922”的错误

    微软Win10 KB5012117更新安装失败 出现了"0x800f0922"的错误 这个错误 有很多大佬都有了办法.但是在我这都试过了仍然还是这个错误. 之后,发现 是mircr ...

  5. 计算机策略更新失败怎么办,大神教你解决win10系统自动更新安装失败的途径

    许多win10系统用户在工作中经常会遇到win10系统自动更新安装失败的情况,比如近日有用户到本站反映说win10系统自动更新安装失败的问题,但是却不知道要怎么解决win10系统自动更新安装失败,我们 ...

  6. 电脑视频显示服务器运行失败,抖音直播伴侣提示:服务器终点无法运行操作或者创建视频源失败请重试解决方法视频教程...

    1.抖音直播伴侣多开,如果提示:服务器终点无法运行操作,或者创建视频源失败,请重试.多开插件一般是操作系统不兼容.要换操作系统,或者安装虚拟机.我推荐的操作系统是WIN10 64位,我拿了100多个操 ...

  7. element vue 动态单选_软件更新丨vue-element-admin 4.0.0 beta 发布,后台集成方案

    vue-element-admin 4.0.0 beta 发布了. vue-element-admin 是一个后台集成解决方案,它基于 vue 和 element.它使用了最新的前端技术栈,内置了 i ...

  8. 计算机更新安装失败,电脑更新安装失败

    C:\WINDOWS\system32>DISM.exe /Online /Cleanup-image /checkhealth 部署映像服务和管理工具 版本: 10.0.14393.0 映像版 ...

  9. IE11安装需要获取更新-安装失败

    一般公司局域网和公网隔离了.用win7(万年不更)安装IE11要报"需要获取更新"提示,然后不能安装! 首先微软官网有个 IE11必备更新. 大致是: 下载下来,依次安装,然后重启 ...

最新文章

  1. 技术图文:如何在leetcode上进行算法刻意练习?
  2. 毕设不会做,怎么办??
  3. 基于FPGA的LED 16×16点阵汉字显示设计
  4. 每年圣诞海报是躲不掉的,趁时间还来得及,看看这里PSD分层模板
  5. java取multipart_spring的multipartResolver和java后端获取的MultipartHttpServletRequest方法对比...
  6. java web项目_一个完整JavaWeb项目开发总结
  7. MySQL社区版下载地址
  8. IJCAI 2022 | 求同存异:多行为推荐的自监督图神经网络
  9. Linux(CentOS)如何上外网
  10. [MATLAB App Designer] 多窗口 App 中的交互(含数据传递)
  11. ubuntu 16.04 LTS 安装搜狗拼音输入法步骤详解
  12. alsa 驱动介绍及user层到hw层文件ioctl操作流程分析
  13. 5.3 常见的电感式和电容式感测原理及应用
  14. 16、【易混淆概念集】-第十章 沟通方法 会议 沟通渠道计算 沟通管理计划
  15. angular 内置管道和自定义管道
  16. xplorer2 Pro(资源管理器) v5.0.0
  17. 计算机水冷科学吗,差价一倍的水冷性能究竟差多少?真相让你大吃一惊!
  18. 详解数仓中的数据分层:ODS、DWD、DWM、DWS、ADS
  19. Java实现RSA加密算法
  20. 什么软件可以测试小米手机电池,小米10使用9个月电池测试

热门文章

  1. Python排序 插入排序
  2. Golang web filter 轻量级实现
  3. 连载17:软件体系设计新方向:数学抽象、设计模式、系统架构与方案设计(简化版)(袁晓河著)...
  4. WinForm DataGridView新增加行
  5. POI Excel 合并数据相同的行
  6. Hyper-V 2016 系列教程34 在局域网内架设Windows时间服务器
  7. 一个技术开发者经常访问的网站
  8. Intel亚太研发有限公司段建钢:存储市场的那些年
  9. Poj1995--Raising Modulo Numbers(快速幂)
  10. 第二十六讲:tapestry的树状(tree)组件