一、性能检测工具工具(Dokit)
注意:官方提醒 👉🏻DoKit的所有功能都只针对Debug环境,Release环境未经过实际验证(推荐单独创建test分支,需要检测的版本内容 merge to test,test 不合并到 其他分支,只用来测试)
1.集成方式
1.1 cocoapods依赖
pod 'DoraemonKit/Core', '~> 3.0.4', :configurations => ['Debug'] #必选
pod 'DoraemonKit/WithGPS', '~> 3.0.4', :configurations => ['Debug'] #可选
pod 'DoraemonKit/WithLoad', '~> 3.0.4', :configurations => ['Debug'] #可选
pod 'DoraemonKit/WithLogger', '~> 3.0.4', :configurations => ['Debug'] #可选
pod 'DoraemonKit/WithDatabase', '~> 3.0.4', :configurations => ['Debug'] #可选
pod 'DoraemonKit/WithMLeaksFinder', '~> 3.0.4', :configurations => ['Debug'] #可选
pod 'DoraemonKit/WithWeex', '~> 3.0.4', :configurations => ['Debug'] #可选
subspec | 功能介绍(说明文档) |
---|---|
Core | 作为核心,必须引入,其他几个subspec你可以按照你的需求选择性接入。 |
WithGPS | 模拟定位功能 |
WithLoad | Load耗时检测的话 |
WithLogger | 基于CocoaLumberjack的日志 |
WithDatabase | 基于YYDebugDatabase可以在网页端调式数据库的话 |
WithMLeaksFinder | 基于MLeaksFinder查找内存泄漏 |
WithWeex | Weex的相关专项工具 |
1.2: Carthage依赖
git "https://github.com/didi/DoraemonKit.git" "c3.0.0"
tip:只在Debug环境中进行集成,不要带到线上。有一些hook操作会污染线上代码。
2.项目集成
#ifdef DEBUG
#import <DoraemonKit/DoraemonManager.h>
#endif
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#ifdef DEBUG
[[DoraemonManager shareInstance] installWithPid:@"productId"];//productId为在“平台端操作指南”中申请的产品id
#endif
}
通过以上步骤你就可以使用DorameonKit
所有的内置工具集合。如果你想把自己与业务相关的一些工具代码加入到DoraemonKit
中做统一管理的话,你可以按照3的步骤来做。
3. 添加自定义测试模块到Doraemon面板中(非必要)
比如我们要在Doraemon面板中添加一个环境切换的功能。
第一步:新建一个类,实现DoraemonPluginProtocol协议中的pluginDidLoad方法,该方法就是以后点击Doraemon工具面板中“环境切换”按钮触发的事件。
比如以代驾司机端为例,点击按钮之后会进入环境切换页面。
@implementation KDDoraemonEnvPlugin
- (void)pluginDidLoad{
[APP_INTERACOTR.rootNav openURL:@"KDSJ://KDDoraemonSFViewController"];
[[DoraemonManager shareInstance] hiddenHomeWindow];
}
@end
第二步:在Doraemon初始化的地方添加第一步中添加的“环境切换”插件
调用DoraemonManager的以下方法:
[[DoraemonManager shareInstance] addPluginWithTitle:@"环境切换" icon:@"doraemon_default" desc:@"用于app内部环境切换功能" pluginName:@"KDDoraemonEnvPlugin" atModule:@"业务专区"];
依次代表 集成到DoraemonKit面板中的标题,图标,描述,插件名称,和所属于的模块。
注意:
接入过程中,如果接入WithMLeaksFinder
,因为依赖facebook
开源的FBRetainCycleDetector
,它的最后更新时间 on Nov 21, 2017
,比较久了,会有冲突,需要适配,
适配方案:podfile
中添加
post_install do |installer|
## Fix for XCode 12.5
find_and_replace("Pods/FBRetainCycleDetector/FBRetainCycleDetector/Layout/Classes/FBClassStrongLayout.mm",
"layoutCache[currentClass] = ivars;", "layoutCache[(id<NSCopying>)currentClass] = ivars;")
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0'
#消除TwitterCore警告
xcconfig_path = config.base_configuration_reference.real_path
xcconfig = File.read(xcconfig_path)
xcconfig_mod = xcconfig.gsub(/-framework "TwitterCore"/, "")
File.open(xcconfig_path, "w") { |file| file << xcconfig_mod }
if config.name == 'Debug'
config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-Onone']
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Owholemodule'
end
if config.name == 'Test'
config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-Onone']
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Owholemodule'
end
end
end
installer.pods_project.build_configurations.each do |config|
config.build_settings[‘EXCLUDED_ARCHS[sdk=iphonesimulator*]’] = ‘arm64’
end
end
def find_and_replace(dir, findstr, replacestr)
Dir[dir].each do |name|
text = File.read(name)
replace = text.gsub(findstr,replacestr)
if text != replace
puts "Fix: " + name
File.open(name, "w") { |file| file.puts replace }
STDOUT.flush
end
end
Dir[dir + '*/'].each(&method(:find_and_replace))
end
4. 使用说明
接入成功之后样式如下图
接入Dokit之后,可以根据自己需求,单独检测某一项指标,比如 帧率、CPU、内存、卡顿、层级...
同时测试过程中,也可以进行健康体检,体检完成之后将本次体检数据上传至Dokit官网中,查看本次体检结果。如下图
二、静态代码审查工具(OCLint)
OCLint是一个通过检查C,C++或Objective-C代码来提高代码质量、降低错误率的静态代码分析工具,代码通过OCLint检测后,可以发现一些潜在的问题,如:
- 可能的bug:if/else/try/catch/finally 空语句空变量
- 代码无用:并未使用的本地变量和参数
- 代码过于复杂:高复杂度的循环、判断
- 代码冗余:冗余的if判断和多余的括号
- 代码异味:长的方法和长参数列表
- 不好的尝试:反向逻辑、参数重复赋值
静态代码分析是一个很重要的技术发现编译器中那些不可视的缺点,OCLint自动完成这些检测需要依赖以下特点:
- 依赖源代码的抽象语法树来保证精准度和效率,尽可能减少误报,避免有用的结果被跳过;
- 动态加载规则到系统中(甚至是运行期间加载规则);
- 灵活可扩展的配置保证用户可以定制化静态代码检查工具;
- 为了技术问题尽早的被修复,降低维护成本,使用命令行运行命令,在代码开发过程中,对代码进行持续集成和检测;
1. OCLint的安装
1.1 安装OCLint
在安装前,确保安装了 homebrew。然后先执行brew命令安装第三方依赖库-oclint/formulae,之后再安装oclint,具体两个指令如下:
brew tap oclint/formulae
brew install oclint
安装正常完成,即证明OCLint安装成功(使用 oclint --version 查看是否安装成功)
。
DavindeMacBook-Pro:~ davin$ brew tap oclint/formulae
Running `brew update --auto-update`...
==> Tapping oclint/formulae
Cloning into '/usr/local/Homebrew/Library/Taps/oclint/homebrew-formulae'...
remote: Enumerating objects: 58, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 58 (delta 1), reused 3 (delta 1), pack-reused 49
Receiving objects: 100% (58/58), 9.37 KiB | 54.00 KiB/s, done.
Resolving deltas: 100% (13/13), done.
Tapped 1 formula (14 files, 18KB).
DavindeMacBook-Pro:~ davin$ brew install oclint
Warning: Treating oclint as a formula. For the cask, use homebrew/cask/oclint
Warning: You are using macOS 13.
We do not provide support for this pre-release version.
You will encounter build failures with some formulae.
Please create pull requests instead of asking for help on Homebrew's GitHub,
Twitter or any other official channels. You are responsible for resolving
any issues you experience while you are running this
pre-release version.
==> Downloading https://github.com/oclint/oclint/releases/download/v20.11/oclint-20.11-llvm-11.0.0-x8
######################################################################## 100.0%
==> Installing oclint from oclint/formulae
🍺 /usr/local/Cellar/oclint/20.11: 488 files, 140.2MB, built in 6 seconds
==> Running `brew cleanup oclint`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> `brew cleanup` has not been run in the last 30 days, running now...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
DavindeMacBook-Pro:~ davin$ oclint --version
OCLint (http://oclint.org/):
OCLint version 20.11.
Built Nov 14 2020 (10:19:05).
DavindeMacBook-Pro:~ davin$
1.2 安装xcodebuild
xcodebuild是xcode的编译命令,xcode 下载安装好就已经成功安装了,无需额外安装
1.3 安装xcpretty
需要使用OCLint对日志信息进行分析运行命令,安装xcpretty,使用xcpretty命令分析日志信息。xcpretty是用来格式化xcodebuild输出的工具,使用ruby开发。安装:
gem install xcpretty
安装完成👇🏻
DavindeMacBook-Pro:~ davin$ gem install xcpretty
Fetching xcpretty-0.3.0.gem
Fetching rouge-2.0.7.gem
Successfully installed rouge-2.0.7
Successfully installed xcpretty-0.3.0
Parsing documentation for rouge-2.0.7
Installing ri documentation for rouge-2.0.7
Parsing documentation for xcpretty-0.3.0
Installing ri documentation for xcpretty-0.3.0
Done installing documentation for rouge, xcpretty after 1 seconds
2 gems installed
2. OCLint命令行使用
2.1 进入指定项目(测试demo)
DavindeMacBook-Pro:~ davin$ cd Desktop
DavindeMacBook-Pro:OnPro_IOS davin$ cd AnimationTest/
2.2 查看项目基本信息
xcodebuild -list
打印输出
DavindeMacBook-Pro:AnimationTest davin$ xcodebuild -list
2023-04-06 09:52:23.967 xcodebuild[7033:57226] DVTCoreDeviceEnabledState: DVTCoreDeviceEnabledState_Disabled set via user default (DVTEnableCoreDevice=disabled)
Command line invocation:
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -list
User defaults from command line:
IDEPackageSupportUseBuiltinSCM = YES
Information about project "AnimationTest":
Targets:
AnimationTest
AnimationTestTests
AnimationTestUITests
Build Configurations:
Debug
Release
If no build configuration is specified and -scheme is not passed then "Release" is used.
Schemes:
AnimationTest
2.3 编译项目
先clean指定项目HDLOnPro,因为集成了pod,所以使用HDLOnPro.xcworkspace;然后再Debug 编译项目了;最后通过xcpretty,使用 -r json-compilation-database 可以生成指定格式的数据。编译成功后,会在项目的文件夹下出现 compile_commands.json 文件
xcodebuild -scheme AnimationTest -workspace AnimationTest.xcworkspace clean && xcodebuild -scheme AnimationTest -workspace AnimationTest.xcworkspace -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json
编译成功会出现▸ Build Succeeded
字样
注意项
- 如果项目使用了 Cocopod,则需要指定 -workspace xxx.workspace
- 每次编译之前需要 clean
(xcodebuild clean)
- 如果你的xcode 版本为14.3可能会出现下面错误
❌ ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneos.a
❌ clang: error: linker command failed with exit code 1 (use -v to see invocation)
解决方案:
在podfile文件中添加
post_install do |installer|
installer.generated_projects.each do |project|
project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
end
pod install
后重新执行编译命令
2.4 生成 html 报表
- 使用
oclint-json-compilation-database
命令对上一步生成的json数据进行分析,对项目代码进行分析,最终生成report.html文件。OCLint目前支持输出html,json,xml,pmd,Xcode格式文件
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html
error1
oclint: error: compilation contains multiple jobs:
解决方案:
将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO
podfile中添加
config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
end
- 看到有报错,但是报错信息太多了,不好定位,利用下面的脚本则可以将报错信息写入 log 文件,方便查看
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log
- 如果项目工程太大,整个 lint 会比较耗时,所幸 oclint 支持针对某个代码文件夹进行 lint
oclint-json-compilation-database -i 需要静态分析的文件夹或文件 -- -report-type html -o oclintReport.html 其他的参数
-
如有错误可根据下一小节内容进行修改,或查找其他资料解决。执行成功后,查看 html 文件可以具体定位哪个代码文件,哪一行哪一列有什么问题,方便修改
三、静态代码审查(infer)
Infer 是一个Facebook 的静态分析工具。Infer 可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题。
Infer捕捉的bug类型
- C/OC中捕捉的bug类型
- 1:Resource leak
- 2:Memory leak
- 3:Null dereference
- 4:Premature nil termination argument
- 只在 OC中捕捉的bug类型
- 1:Retain cycle
- 2:Parameter not null checked
- 3:Ivar not null checked
Infer优点
- 效率高,规模大,几分钟能扫描数千行代码;
- 支持增量及非增量分析(后边会解释)
- 分解分析,整合输出结果。(infer能将代码分解,小范围分析后再将结果整合在一起,兼顾分析的深度和速度)
Infer的安装与使用
brew、xcpretty 、xcodebuild同上,都是需要安装的
1. 下载infer:https://github.com/facebook/infer
2. 环境依赖
brew install autoconf automake cmake opam pkg-config sqlite gmp mpfr java
3. 安装infer
# Checkout Infer
git clone https://github.com/facebook/infer.git
cd infer
# Compile Infer
./build-infer.sh clang
# install Infer system-wide...
sudo make install
# ...or, alternatively, install Infer into your PATH
export PATH=`pwd`/infer/bin:$PATH
4. 将infer的执行目录配置到环境变量
# 将infer/bin添加到.bash_profile或.zshrc文件中
export PATH="$PATH:[替换为infer-main保存的路径]/infer/bin"
# 修改.bash_profile文件,并更新环境变量 (适用于: Linux、macOS Mojave 之前的系统)
open -e $HOME/.bash_profile
source $HOME/.bash_profile
# 修改.zshrc文件,并更新环境变量 (适用于:macOS Catalina)
open -e $HOME/.zshrc
source $HOME/.zshrc
5. 验证是否安装成功:
infer --version
infer使用
#先清理缓存
xcodebuild -scheme AnimationTest -workspace AnimationTest.xcworkspace clean
#编译数据 database
infer -- xcodebuild -workspace AnimationTest.xcworkspace -scheme AnimationTest -configuration Debug
#xcodebuild -workspace AnimationTest.xcworkspace -scheme AnimationTest -sdk iphonesimulator -arch x86_64 COMPILER_INDEX_STORE_ENABLE=NO| tee xcodebuild.log | xcpretty -r json-compilation-database -o compile_commands.json
# 执行infer指令
infer run --keep-going --skip-analysis-in-path Pods --compilation-database-escaped compile_commands.json
# 扫描生成报告
infer-explore --html
结果👇🏻
四、书写习惯&非空判断
日常写代码的时候多注意非空判断,多注意数据越界,
1. 通过Category处理了一些nil的情况,可以起到防crash的效果
参考demo:WXWCategory
example:获取字典键值对时
- (NSArray *)getArray:(NSString *)name {
id value = [self replaceValue:name];
if (value == nil || value == [NSNull null]) {
return nil;
}
if ([value isKindOfClass:[NSArray class]]) {
return value;
}
return nil;
}
- (NSString *)getString:(NSString *)name {
id value = [self replaceValue:name];
if (value == nil || value == [NSNull null]) {
return @"";
}
if ([[value description] isEqualToString:@"(null)"]) {
return @"";
}
if ([value isKindOfClass:[NSString class]]) {
return (NSString*)value;
}
if ([value isKindOfClass:[NSNumber class]]) {
return [value stringValue];
}
return @"";
}
- (NSDictionary *)getDictionary:(NSString *)name {
id value = [self replaceValue:name];
if (value == nil || value == [NSNull null]) {
return nil;
}
if ([value isKindOfClass:[NSDictionary class]]) {
return value;
}
return nil;
}
2. 数组下标获取元素前判空
- (void)configCell:(ChatMessageCell *)cell indexRow:(NSIndexPath *)indexPath {
if (self.mMessageArray.count <= indexPath.row) return;
MessageModel *messageModel = self.mMessageArray[indexPath.row];
[cell congifAIFriendAvatar:self.friendModel.headPortrait];
[cell configWithMessage:messageModel.message isQuesstion:messageModel.isQuestion askTime:messageModel.askTime isLoading:messageModel.isLoading];
}
五、Crash自动防护(XXShield)
XXShield 库有两个重要的功能:
- 防止Crash
- 捕获异常状态下的崩溃信息
防止Crash包含一下几种类型(详细说明)
1. Unrecognized Selector Crash
2. KVO Crash
3. Container Crash
4. NSNotification Crash
5. NSNull Crash
6. NSTimer Crash
7. 野指针 Crash
1. XXShield 集成
cocoaPods接入
pod "XXShield"
2.使用
/**
注册汇报中心
@param record 汇报中心
*/
+ (void)registerRecordHandler:(id<XXRecordProtocol>)record;
/**
注册SDK,默认只要开启就打开防Crash,如果需要DEBUG关闭,请在调用处使用条件编译
本注册方式不包含EXXShieldTypeDangLingPointer类型
*/
+ (void)registerStabilitySDK;
/**
本注册方式不包含EXXShieldTypeDangLingPointer类型
@param ability ability
*/
+ (void)registerStabilityWithAbility:(EXXShieldType)ability;
/**
///注册EXXShieldTypeDangLingPointer需要传入存储类名的array,暂时请不要传入系统框架类
@param ability ability description
@param classNames 野指针类列表
*/
+ (void)registerStabilityWithAbility:(EXXShieldType)ability withClassNames:(nonnull NSArray<NSString *> *)classNames;
常见crash 测试案例:(添加XXShield之后不会出现崩溃)
#warning mock crash
[XXShieldSDK registerStabilitySDK];
// Unrecoginzed Selector Crash (点击按钮之后不会出现崩溃)
UIButton *aaa = [UIButton buttonWithType:UIButtonTypeCustom];
aaa.frame = CGRectMake(200, CGRectGetHeight(self.view.bounds) - 200, 100, 40);
[aaa setTitle:@"测试" forState:UIControlStateNormal];
[aaa addTarget:self action:@selector(aaaAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:aaa];
// NSNull Crash (setObject:nil 不会出现崩溃)
NSMutableDictionary *dictM = [[NSMutableDictionary alloc] init];
[dictM setObject:nil forKey:@"xxshild"];
NSLog(@"############:%@", [dictM objectForKey:@"xxshild"]);