写点什么

基于 clang 插件的一种 iOS 包大小瘦身方案

2017 年 1 月 08 日

引子

包瘦身,包瘦身,包瘦身,重要的事情说三遍。

最近公司一款 iOS APP(本文只讨论使用 Objective C 开发的 iOS 安装包) 一直在瘦身,我们团队的 APP 也愈发庞大了。而要解决这个问题,思路主要集中在两个方向,资源和代码。资源主要在于图片,方法包括移除未被引用的图片,只使用一套图片 (2x 或 3x),图片伸缩等;代码层面主要思路包括重构消除冗余,linkmap 中 selector 引用分析等。除此之外,有没有别的路径呢?

众所周知,代码之间存在调用关系。假设 iOS APP 的主入口为 -[UIApplication main], 则所有开发者的源代码 (包括第三方库) 可分为两类: 存在一条调用路径,使得代码可以被主入口最终调用 (称此类代码为被最终调用);不存在一条调用路径,使得代码最终不能被主入口调用 (称此类代码为未被最终调用)。

假设有一个源代码级别的分析工具 (或编译器),可以辅助分析代码间的调用关系,这样就使得分析最终被调用代码成为可能,剩下的就是未被最终调用的代码。

这种工具目前有成熟可用的吗?答案是肯定的,就是 clang 插件。除可用于分析未被最终调用代码外,clang 还可辅助发现重复代码。

LLVM 与 clang 插件

LLVM 工程包含了一组模块化,可复用的编辑器和工具链。同其名字原意 (Low Level Virtual Machine) 不同的是,LLVM 不是一个首字母缩写,而是工程的名字。目前 LLVM 包含的主要子项目包括:

  1. LLVM Core: 包含一个现在的源代码 / 目标设备无关的优化器,一集一个针对很多主流 (甚至于一些非主流) 的 CPU 的汇编代码生成支持。
  2. Clang: 一个 C/C++/Objective-C 编译器,致力于提供令人惊讶的快速编译,极其有用的错误和警告信息,提供一个可用于构建很棒的源代码级别的工具.
  3. dragonegg: gcc 插件,可将 GCC 的优化和代码生成器替换为 LLVM 的相应工具。
  4. LLDB: 基于 LLVM 提供的库和 Clang 构建的优秀的本地调试器。
  5. libc++、libc++ ABI: 符合标准的,高性能的 C++ 标准库实现,以及对 C++11 的完整支持。
  6. compiler-rt: 针对__fixunsdfdi和其他目标机器上没有一个核心 IR(intermediate representation) 对应的短原生指令序列时,提供高度调优过的底层代码生成支持。
  7. OpenMP: Clang 中对多平台并行编程的 runtime 支持。
  8. vmkit: 基于 LLVM 的 Java 和.NET 虚拟机实
  9. polly: 支持高级别的循环和数据本地化优化支持的 LLVM 框架。
  10. libclc: OpenCL 标准库的实现
  11. klee: 基于 LLVM 编译基础设施的符号化虚拟机
  12. SAFECode: 内存安全的 C/C++ 编译器
  13. lld: clang/llvm 内置的链接器

作为 LLVM 提供的编译器前端,clang 可将用户的源代码 (C/C++/Objective-C) 编译成语言 / 目标设备无关的 IR(Intermediate Representation) 实现。其可提供良好的插件支持,容许用户在编译时,运行额外的自定义动作。

我们的目标是使用 clang 插件减少包大小。其原理是,针对目标工程,基于 clang 的插件特性,开发者可以编写插件以分析所有源代码。编译过程中,将插件作为 clang 的参数载入并生成各种中间文件。编译完成后,还需编写一个工具去分析所有包含源码的方法 (包括用户编写,以及引入的第三方库源代码),检查这些方法中哪些最终可被程序主入口调用,剩余即是疑似无用代码。简单的一个复查,移除那些确定无用的代码,重新编译,便可以有效去除无用的代码从而减少包大小。

本文相关内容如下:

  1. 如何编写一个 clang 插件并集成到 Xcode
  2. 如何实现代码级别的包瘦身
  3. 局限与个性化定制
  4. 其他

如何编写一个 clang 插件并集成到 Xcode

Clone clang 源码并编译安装

复制代码
cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`
git clone -b release_39 git@github.com:llvm-mirror/llvm.git llvm
git clone -b release_39 git@github.com:llvm-mirror/clang.git llvm/tools/clang
git clone -b release_39 git@github.com:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_39 git@github.com:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt
mkdir llvm_build
cd llvm_build
cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release
make -j`sysctl -n hw.logicalcpu`

编写 clang 插件

要实现自定义的 clang 插件 (以 C++ API 为例),应按照以下步骤:

  1. 自定义继承自 clang::PluginASTAction(基于 consumer 的抽象语法树 (Abstract Syntax Tree/AST) 前端 Action 抽象基类)

clang::ASTConsumer(用于客户读取抽象语法树的抽象基类),

clang::RecursiveASTVisitor(前序或后续地深度优先搜索整个抽象语法树,并访问每一个节点的基类) 等基类。
2. 根据自身需要重载 PluginASTAction::CreateASTConsumer

PluginASTAction::ParseArgs

ASTConsumer::HandleTranslationUnit

RecursiveASTVisitor::VisitDecl

RecursiveASTVisitor::VisitStmt

等方法,实现自定义的分析逻辑。
3. 注册插件 static FrontendPluginRegistry::Add<MyPlugin> X("my-plugin- name", "my-plugin-description");

更多 clang 插件: http://clang.llvm.org/docs/ExternalClangExamples.html

编译生成插件 (dylib)

假定你的 clang 插件源文件为 your-clang-plugin-source.cpp,需生成的插件名为 your-clang-plugin-name.dylib,可以使用如下命令 (载入了 llvm,clang 的 include 路径,生成的相关 lib 等) 生成:

复制代码
clang -std=c++11 -stdlib=libc++ -L/opt/local/lib -
L/opt/llvm/llvm_build/lib -I/opt/llvm/llvm_build/tools/clang/include -
I/opt/llvm/llvm_build/include -I/opt/llvm/llvm/tools/clang/include -
I/opt/llvm/llvm/include -dynamiclib -Wl,-headerpad_max_install_names -lclang -
lclangFrontend -lclangAST -lclangAnalysis -lclangBasic -lclangCodeGen -
lclangDriver -lclangFrontendTool -lclangLex -lclangParse -lclangSema -
lclangEdit -lclangSerialization -lclangStaticAnalyzerCheckers -
lclangStaticAnalyzerCore -lclangStaticAnalyzerFrontend -lLLVMX86CodeGen -
lLLVMX86AsmParser -lLLVMX86Disassembler -lLLVMExecutionEngine -lLLVMAsmPrinter
-lLLVMSelectionDAG -lLLVMX86AsmPrinter -lLLVMX86Info -lLLVMMCParser -
lLLVMCodeGen -lLLVMX86Utils -lLLVMScalarOpts -lLLVMInstCombine -
lLLVMTransformUtils -lLLVMAnalysis -lLLVMTarget -lLLVMCore -lLLVMMC -
lLLVMSupport -lLLVMBitReader -lLLVMOption -lLLVMProfileData -lpthread -lcurses
-lz -lstdc++ -fPIC -fno-common -Woverloaded-virtual -Wcast-qual -fno-strict-
aliasing -pedantic -Wno-long-long -Wall -Wno-unused-parameter -Wwrite-strings
-fno-rtti -fPIC your-clang-plugin-source.cpp -o your-clang-plugin-name.dylib

与 Xcode 集成

下载 XcodeHacking.zip: https://raw.githubusercontent.com/kangwang1988/kangwang1988.github.io/master/others/XcodeHacking.zip

使用命令行编译时,可以用如下方式载入插件:

复制代码
clang++ *** -Xclang -load -Xclang path-of-your-plugin.dylib -Xclang -add-
plugin -Xclang your-pluginName -Xclang -plugin-arg-your-pluginName -Xclang
your-pluginName-param
{1}

要在 Xcode 中使用 clang 插件,需要如下 hack Xcode.

复制代码
sudo mv HackedClang.xcplugin xcode-select -print-
path/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec xcode-select -print-
path/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

在 Xcode->Target-Build Settings->Build Options->Compiler for C/C++/Objective-C 选择 Clang LLVM Trunk 即可使得 Xcode 使用上文生成的的 clang 来编译。至于其他命令行参数均可通过 Xcode 中的编译选项设置完成。

如何实现代码级别的包瘦身

本文所说的代码指的是 OC 中的形如-/+[Class method:\*]这种形式的代码,调用关系典型如下:

复制代码
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor redColor]];
}
@end

则称:-[ViewController viewDidLoad]调用了:

-[UIViewController viewDidLoad]

-[ViewController view](语法糖)

+[UIColor redColor]

-[UIView setBackgroundColor:]

这种调用关系可在 clang 遍历抽象语法树的时候得到。由于编译器访问抽象语法树时存在嵌套关系,如上例:编译器在访问类实现 ViewController 的时候,嵌套了访问-[ViewController viewDidLoad]的方法实现, 而在访问-[ViewController viewDidLoad]的方法实现的时候,嵌套了访问消息发送-[UIViewController viewDidLoad](对应源码[super viewDidLoad]),-[ViewController view](对应源码self.view),+[UIColor redColor](对应源码[UIColor redColor]),-[UIView setBackgroundColor:](对应源码[self.view setBackgroundColor:[UIColor redColor]]) 等,这样通过记录相关信息即可了解我们关注的方法间调用关系。

数据结构

为了分析调用关系,用到的中间数据结构如下:

类接口与继承体系 (clsInterfHierachy)

此数据结构记录了所有位于抽象语法树上的接口内容,最终的解析结果如下图所示:

以 AppDelegate 为例,interfs 代表其提供的接口 (注: 它的 property window 对应的 getter 和 setter 也被认为是 interf 一部分);isInSrcDir 代表此类是否位于用户目录 (将 workspace 的根目录作为参数传给 clang) 下,protos 代表其遵守的协议,superClass 代表接口的父类。

这些信息获取入口位于VisitDecl(Decl \*decl)的重载函数里,相关的 decl 有:

  • ObjCInterfaceDecl(接口声明)
  • ObjCCategoryDecl(分类声明)
  • ObjCPropertyDecl(属性声明)
  • ObjCMethodDecl(方法声明)

接口方法调用 (clsMethod)

此数据结构记录了所有包含源代码的 OC 方法,最终解析结果如下所示:

(点击放大图像)

-[AppDelegate application:didFinishLaunchingWithOptions:]为例,callee 代表其调用到的接口 (此处为可以明确类型的,对于形如id\<XXXDelegate\>后文介绍),filename 为此方法所在的文件名,range 为方法所在的范围,sourceCode 为方法的具体实现源代码。

这些信息获取入口位于VisitDecl(Decl \*decl)VisitStmt(Stmt \*stmt)的重载函数里,相关的 decl 有ObjCMethodDecl(方法声明),stmt 有ObjCMessageExpr(消息表达式)

此处除过正常的-/+[Class method:\*]外,还有其他较多的需要考虑的情形,已知且支持的分析包括:

  • NSObject 协议的 performSelector 方法簇 [obj performSelector:@selector(XXX)]不仅包含[obj performSelector:]也包含[obj XXX].(下同)
  • 手势 / 按钮的事件处理 selector addTarget:action:/initWithTarget:action:/addTarget:action:forControlEvents:
  • NSNotificationCener 添加通知处理 Selector addObserver:selector:name:object:
  • UIBarButtonItem 添加事件处理 Selector <action: initwithbarbuttonsystemitem:target:action:="" p="">< initWithTitle:style:target:action: style:target:action: initWithImage:landscapeImagePhone:></action:>
  • Timer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: /timerWithTimeInterval:target:selector:userInfo:repeats:/initWithFireDate: interval:target:selector:userInfo:repeats:
  • NSThread detachNewThreadSelector:toTarget:withObject:/initWithTarget:selector:object:
  • CADisplayLink displayLinkWithTarget:selector:
  • KVO 机制 addObserver:forKeyPath:options:context:, 不同于别的都要处理方法本身调用和对应 target:selector 调用,这里 KVO 的 addObserver 则暗含了observeValueForKeyPath:ofObject:change:context:
  • IBAction 机制 如基于 xib/Storyboard 的 ViewController 中-(IBAction)onBtnPressed:(id)sender方法,认为暗含了+[ViewController 的 alloc]对于+[ViewController 的 onBtnPressed:]的调用关系。
  • [XXX new]包含+[XXX alloc]-[XXX init]

协议的接口与继承体系 (protoInterfHierachy)

此数据结构记录了所有位于抽象语法树上的协议内容,最终的解析结果如下图所示:

其中各字段定义同 clsInterfHierachy.

这些信息获取入口位于VisitDecl(Decl \*decl)的重载函数里,相关的 decl 有:

  • ObjCProtocolDecl(协议声明)
  • ObjCPropertyDecl(属性声明)
  • ObjCMethodDecl(方法声明)

协议方法的调用 (protoInterfCall)

此数据结构记录了所有如:-[ViewController func1]调用了-[id\<ViewControllerDelegate\> viewController:execFunc:]的形式,最终结果如下所示:

这些信息获取入口位于VisitStmt(Stmt \*stmt)的重载函数里,相关的 stmt 是ObjCMessageExpr.

添加通知

以第一条记录为例,其意思是说 -[AppDelegate onViewControllerDidLoadNotification:] 作为通知 kNotificationViewControllerDidLoad 的 Selector,在 -[AppDelegate application:didFinishLaunchingWithOptions:] 中被添加。

(点击放大图像)

发送通知

第一条记录中,作为系统级别的通知,将被认为被APP 主入口调用。

第二条记录则说明了, -[ViewController viewDidLoad]发送了 kNotificationViewControllerDidLoad。

如果-[AppDelegate application:didFinishLaunchingWithOptions:]-[UIApplication main](假定的主入口) 调用,且-[ViewController viewDidLoad]被调用,则-[AppDelegate onViewControllerDidLoadNotification:]被调用。其中,如果通知是系统通知,则只需要-[AppDelegate application:didFinishLaunchingWithOptions:]被调用即可。

这些信息获取入口位于VisitStmt(Stmt \*stmt)的重载函数里,相关的 stmt 有ObjCMessageExpr. 为了简单处理,此处只处理形如addObserver:self这种 (也是最常见的情况),否则 Argu 作为Expr\*分析起来会很复杂。PS. 系统通知和本地通知的区别使用了名称上的匹配 (系统通知常以 NS,UI,AV 开头以 Notification 结束).

重复代码分析

此处的重复代码针对的是某两个 (或两个以上)-/+[Class method:\*]的实现是一模一样的。参考上文提到的 clsMethod 中的 sourceCode,可以获得每一个方法实现的源代码。同时为了消除诸如格式上的差异 (如多了一个空格,少了一个空格之类) 引起的差异,先基于 clang 提供的 format 功能,按照某种风格 (google/llvm 等) 将所有方法实现源码格式化,再进行分析即可。

使用 LLVM 风格将代码 format:

find $prjDir -type f -name "\*.m" | xargs /opt/llvm/llvm_build/bin/clang-format -i -style=LLVM本文示例工程得到的一个重复代码结果如下所示:

(点击放大图像)

未被最终调用代码分析

分析的对象在于 clsMethod.json 里面所有的 key,即实际拥有源代码的所有方法。

  1. 初始化默认的调用关系 usedClsMethodJson:{-[AppDelegate alloc],"-[UIApplication main]","-[UIApplication main]","-[UIApplication main]","+[NSObject alloc]","-[UIApplication main]"}, 其中 AppDelegate 由用户传给 Analyzer.
  2. 分析所有含源码方法是否存在一条路可以被已经调用 usedClsMethodJson 中的 key 调用。

对于某一个 clsMethod,其需要检查的路径包括三个,类继承体系,协议体系和通知体系。

针对类继承体系,从当前类一直向上追溯 (直到发现有被调用或者 NSObject),每一个基类对应的-/+[Class method:*]是否被隐含的调用关系所调用,如-[ViewController viewDidLoad]-[ViewController alloc]隐含调用,当-[ViewController alloc]已经被调用的时候,-[ViewController viewDidLoad]也将被认为调用。这里需要注意需要写一个隐含调用关系表以供查询,如下所示:

针对 Protocol 体系,需要参考类似 Protocol 引用体系向上追溯 (直到发现有被调用或者NSObject协议),针对某一个特定的 Protocol 判断的时候,需要区分两种,一种是系统级的 Protocol,如UIApplicationDelegate,对于-[AppDelegate application:didFinishLaunchingWithOptions:]这种,参考AppDelegate<UIApplicationDelegate>,如果-[AppDelegate alloc]被调用则认为-[AppDelegate application:didFinishLaunchingWithOptions:]被调用。针对用户定义的 Protocol, 如ViewControllerDelegate,对于-[AppDelegate viewController:execFunc:]不仅需要-[AppDelegate alloc]被调用并且 protoInterfCall.json 中-[ViewControllerDelegate viewController:execFunc:]对应的 Callers 有已经存在于 usedClsMethodJson 的 Caller.

针对通知体系,前文已经有过分析。

本例分析使用到的 ClsMethod 结果如下:

(点击放大图像)

本例分析未被使用到的ClsMethod 结果如下:

查看示例工程: https://github.com/kangwang1988/XcodeZombieCode.git

zulip-ios 的应用效果对比

鉴于示例工程规模较小,另选取开源的 zulip-ios 工程,其中原始工程 Archive 生成的可执行文件大小为 3.4MB,结合本文所述方法去除未被最终调用的代码 (包括业务代码,第三方库) 后,可执行文件变为 3MB。对于这样一个设计良好的工程,纯代码的瘦身效果还是比较可观的。

局限与个性化定制

这种静态分析适合可以判断出消息接收者类型的情况,面对运行时类型和静态分析类型不一致,或者静态分析不出来类型时,不可用。这种分析要求代码书写规范。例如一个 Class 实现了某个 Protocol,一定要在声明里说明,或者 Property 中 delegate 是id<XXXDelegate>的时候也要注明。

虽然此项目已经给了一个完整的重复代码和无用代码分析工具,但也有其局限性 (主要是动态特性)。具体分析如下:

  1. openUrl 机制 假设工程设置里使用了openUrl:"XXX://XXViewController"来打开一个 VC,则 Clang 插件里面需要分析 openUrl 的参数,如果参数是 XXViewController,则暗含了+[XXViewController alloc]-[XXViewController init].
  2. Model 转化 如如果 MTLModel 使用到了modelOfClass:[XXXModel class] fromJSONDictionary:error:,则暗含了+[XXXModel alloc]+[XXXModel init].
  3. Message swizzle 假设用户 swizzle 了-[UIViewController viewDidLoad]-[UIViewController XXviewDidLoad],则需要在 implicitCallStackJson 中添加-[UIViewController XXviewDidLoad],-[UIViewController viewDidLoad].
  4. 第三方 Framework 暗含的逻辑 如高德地图的 AnnotationView, 需要 implicitCallStackJson 中添加"-[MAAnnotationView prepareForReuse:]","+[MAAnnotationView alloc]"等。包括第三方 Framework 里面的一些 Protocol,可能也需要参考前文提到的 UIApplicationDelegate 按照系统级别的 Protocol 来处理。
  5. 一些遗漏的重载方法 如-[XXDerivedManager sharedInstance]并无实现,而 XXDerivedManager 的基类 XXBaseManager 的 sharedInstance 调用了-[self alloc], 但因为 self 静态分析时被认定为 XXBaseManager,这就导致-[XXDerivedManager sharedManager]虽然被 usedclsmethod.json 调用,但是-[XXDerivedManager alloc]却不能被调用。这种情况,可以在 usedClsMethodJson 初始化的时候,加入"+[XXDerivedManager alloc]","-[UIApplication main]"
  6. 类似 Cell Class 我们常会使用动态的方法去使用[[[XXX cellClassWithCellModel:] alloc] initWithStyle:reuseIdentifier:]去构造 Cell,这种情况下,应该针对cellClassWithCellModel里面会包含的各种return [XXXCell class],在 implicitCallStackJson 中添加[[XXXCell alloc] initWithStyle:reuseIdentifier:],-[XXX cellClassWithCellModel:]这种调用。
  7. Xib/Storyboard 会暗含一些 UI 元素 (Controller,Table,Button,Cell,View 等) 的 alloc 方法或调用关系。
  8. 其他隐含的逻辑或者动态特性导致的调用关系遗漏。

其他

对于包大小而言,可以参考以下的思路去瘦身代码:

  1. 重复代码的提取重构
  2. 无用代码的移除
  3. 使用率较低的第三方库的处理 (本文不仅可以查找到重复,无用的代码,进一步分析 clsMethod.json/unusedClsMethod.json 更可以获取到每一个 framework 里面有多少个方法,各方法有多少代码,多少个方法又被-[UIApplication main]调用到了),面对使用率很低的库,需要考虑是不是要全部引入或者重写。
  4. 重复引用的第三方库的处理 (曾经发现团队项目的工程里面引用了其他团队的库,但由于多个库里面均有一份自己的 Zip 的实现,面对这种情况,可以考虑将此种需求全部抽象出来一个公共的 Framework 去处理,其他人都引用此项目,或者干脆使用系统本身自带的 libz 去处理会更好些)。

因为可在源码级别分析,使用 clang 插件可做的工作很多。笔者还使用了 clang 插件去实现了代码风格检查,API 有效性验证,相关示例项目如下:

代码风格检查: https://github.com/kangwang1988/XcodeCodingStyle.git

API 有效性验证: https://github.com/kangwang1988/XcodeValidAPI.git


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017 年 1 月 08 日 16:527067

评论

发布
暂无评论
发现更多内容

Java创建对象的方法有哪些?

古月木易

Java

一条龙!CI / CD 、打造小团队前端工程化服务

久违

Vue 前端 jenkins React 前端工程化

为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我明白了

Java迁哥

Java 阿里巴巴 程序员 成长 笔记

实战案例丨使用云连接CC和数据复制服务DRS实现跨区域RDS迁移和数据同步

华为云开发者社区

迁移 灾备 数据复制 云连接 数据同步

【API进阶之路】破圈,用一个API代替10人内容团队

华为云开发者社区

内容 编辑 API 华为云 文本摘要

ArCall远比你想象的要强大的多

anyRTC开发者

WebRTC 在线教育 直播 RTC 安卓

2019年我最喜欢的三款数码产品。

徐说科技

手机 苹果

usdt承兑支付系统开发|usdt跑分系统搭建

WX13823153201

USDT承兑支付系统开发

Mybatis进阶之基础CRUD

不才陈某

mybatis springboot

架构师训练营 - 命题作业 第 12周

铁血杰克

极客大学

手机没网了,却还能支付,这是什么原理?

楼下小黑哥

支付宝 微信支付 支付

Docker 网络模式详解及容器间网络通信

哈喽沃德先生

Docker 容器 微服务 容器技术 容器化

usdt支付跑分系统开发|区块链跨境入金系统搭建

WX13823153201

java安全编码指南之:Mutability可变性

程序那些事

Java java安全编码 编码指南 可变性

面经手册 · 第9篇《队列是什么?什么是双端队列、延迟对列、阻塞队列,全是知识盲区!》

小傅哥

数据结构 小傅哥 队列 ArrayDeque

分析HiveQL 生成的MapReduce执行程序

任小龙

Java中强、软、弱、虚四种引用详解

奈学教育

Java

架构设计复杂度来源

escray

学习 从零开始学架构 架构师预科班

拥抱K8S系列-01-CentOS7安装docker

张无忌

Docker centos 运维

Java中强、软、弱、虚四种引用详解

古月木易

Java

数字货币交易所系统搭建源码|交易所开发公司

WX13823153201

数字货币交易所系统搭建

高效程序员的45个习惯:敏捷开发修炼之道(7)

石云升

敏捷开发 晨会

【运维探讨】RPA落地实践,提升IT运维工作效能!

嘉为蓝鲸

RPA 运维自动化 标准化 系统运维 流程

LR.JAVA平台商业可视化大屏(BI)设计演示

力软.net/java开发平台

软件开发 BI 数据可视化 Java 分布式

拥抱K8S系列-02-服务器部署应用和docker部署应用区别(nginx篇)

张无忌

nginx Docker 运维

CPU中的程序是怎么运行起来的(预告篇)

良知犹存

cpu

vivo商城前端架构升级-总览篇

vivo互联网技术

node.js Vue 前端 架构设计

JVM中unsafe.cpp源码

Darren

c++ 源码 JVM unsafe

JDK8 Unsafe.java 源码

Darren

源码 并发 CAS 代码注释 unsafe

Java创建对象的方法有哪些?

奈学教育

Java

区块链钱包开发方案|多币种钱包搭建app

WX13823153201

区块链钱包开发

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

基于clang插件的一种iOS包大小瘦身方案-InfoQ