客户端(iOS)性能稳定方案

一、性能检测工具工具(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. 使用说明

接入成功之后样式如下图


2707146-5eefd26025403af3.jpeg

接入Dokit之后,可以根据自己需求,单独检测某一项指标,比如 帧率、CPU、内存、卡顿、层级...

同时测试过程中,也可以进行健康体检,体检完成之后将本次体检数据上传至Dokit官网中,查看本次体检结果。如下图


image.png

二、静态代码审查工具(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

image.png

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 文件可以具体定位哪个代码文件,哪一行哪一列有什么问题,方便修改


    image.png

三、静态代码审查(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. 环境依赖
image.png
brew install autoconf automake cmake opam pkg-config sqlite gmp mpfr java
3. 安装infer
image.png
# 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

结果👇🏻


image.png

四、书写习惯&非空判断

日常写代码的时候多注意非空判断,多注意数据越界,

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"]);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容