优化篇

787 阅读9分钟

卡顿优化

CPU和GPU
CPU(Central Processing Unit,中央处理器):

对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)

GPU(Graphics Processing Unit,图形处理器):

纹理的渲染

整个流程图:

image.png

注意:在iOS中是双缓冲机制,有前帧缓存、后帧缓存

屏幕成像原理

image.png

卡顿原因 image.png

解决卡顿的思路:
1.尽可能减少CPU、GPU资源消耗
2.按照60FPS的刷帧率,每隔16ms就会有一次VSync信号

解决:
CPU层面

1.尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
2.不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
3.尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
4.Autolayout会比直接设置frame消耗更多的CPU资源
5.图片的size最好刚好跟UIImageView的size保持一致
6.控制一下线程的最大并发数量
7.尽量把耗时的操作放到子线程(文本处理(尺寸计算、绘制),图片处理(解码、绘制

GPU层面

1.尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
2.GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
3.尽量减少视图数量和层次
4.减少透明的视图(alpha<1),不透明的就设置opaque为YES
5.尽量避免出现离屏渲染

关于离屏渲染

image.png

卡顿检测 LXDAppFluecyMonitor 通过向Runloop注册observer

耗电优化

耗电
1.CPU处理,Processing
2.网络,Networking
3.定位,Location
4.图像,Graphics

耗电优化

image.png

image.png

App启动优化

App启动
APP的启动可以分为2种
1.冷启动(Cold Launch):从零开始启动APP
2.热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP

APP启动时间的优化,主要是针对冷启动进行优化

通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
1.DYLD_PRINT_STATISTICS设置为1
2.如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1

Total pre-main time: 1.0 seconds (100.0%)
         dylib loading time: 574.14 milliseconds (52.5%)
        rebase/binding time:  43.93 milliseconds (4.0%)
            ObjC setup time: 302.11 milliseconds (27.6%)
           initializer time: 172.46 milliseconds (15.7%) 
           slowest intializers :
             libSystem.B.dylib :   8.25 milliseconds (0.7%)
    libMainThreadChecker.dylib :  32.07 milliseconds (2.9%)
                  AFNetworking :  61.96 milliseconds (5.6%)
                  探马回报 :  37.40 milliseconds (3.4%)

ObjC setup time:所能做的就是减少应用中的类,用第三方工具检测
initializer time:大部分是load方法,采用懒加载或者放到类的initializ方法中执行

image.png

dyld阶段

image.png

runtime阶段

image.png

App启动 -main阶段

image.png

注意:在main函数阶段我们可以使用第三方的打点工具BLStopwatch来查看代码的执行时间

启动优化

image.png

pre_main阶段
二进制重排(源自抖音团队)
关于虚拟内存和物理内存?

image.png

操作:

  1. 使用Instrument中的system trace,将应用跑起来
  2. 查看虚拟内存中的File Backed Page In中的count,代表page fault的次数,下图是159次(注意:iOS中一页16k)

image.png

image.png

操作:二进制重排是可以用xcode来进行配置的
我们以objc750源码为例子,在xcode的Build Setting中查找order file,最终会在路径下生成libobjc.order的文件,里面存储的都是符号文件,见下图,苹果编译的动态库,就是按照order来进行排列的,也就是说二进制重排苹果一直是有使用到的

image.png

image.png

同理,我们自己也可以配置一个order文件路径,在这个 order 文件中,将你需要的符号按顺序写在里面,而当当工程 build 的时候 , Xcode 会读取这个文件 , 打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O

那么如何查看我们工程中的符号顺序呢?

在Build Setting中搜索Write Link Map File,将其设置为YES,意思是替我们生成一个符号文件,我们点击Products中的.App,然后show in finder,最终找到它。

image.png

image.png

头部告诉我们具体链接了哪些.o文件,而这个顺序是和Build Phases中的Compile Sources顺序保持一致。后面是

# Object files:
[  0] linker synthesized
[  1] /Users/wangyun/Library/Developer/Xcode/DerivedData/二进制重排2-dfczjgcsvileebfcodpvwnzqxbcn/Build/Intermediates.noindex/二进制重排2.build/Debug-iphoneos/二进制重排2.build/Objects-normal/arm64/ViewController.o
[  2] /Users/wangyun/Library/Developer/Xcode/DerivedData/二进制重排2-dfczjgcsvileebfcodpvwnzqxbcn/Build/Intermediates.noindex/二进制重排2.build/Debug-iphoneos/二进制重排2.build/Objects-normal/arm64/AppDelegate.o
[  3] /Users/wangyun/Library/Developer/Xcode/DerivedData/二进制重排2-dfczjgcsvileebfcodpvwnzqxbcn/Build/Intermediates.noindex/二进制重排2.build/Debug-iphoneos/二进制重排2.build/Objects-normal/arm64/main.o
[  4] /Users/wangyun/Library/Developer/Xcode/DerivedData/二进制重排2-dfczjgcsvileebfcodpvwnzqxbcn/Build/Intermediates.noindex/二进制重排2.build/Debug-iphoneos/二进制重排2.build/Objects-normal/arm64/SceneDelegate.o
[  5] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.3.sdk/System/Library/Frameworks//Foundation.framework/Foundation.tbd
[  6] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.3.sdk/usr/lib/libobjc.tbd
[  7] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.3.sdk/System/Library/Frameworks//UIKit.framework/UIKit.tbd

image.png

image.png

image.png

注意:
上述文件中最左侧地址就是 实际代码地址而并非符号地址 , 因此我们二进制重排并非只是修改符号地址 , 而是利用符号顺序 , 重新排列整个代码在文件的偏移地址 , 将启动需要加载的方法地址放到前面内存页中 , 以此达到减少 page fault 的次数从而实现时间上的优化 , 一定要清楚这一点 . 你可以利用 MachOView 查看排列前后在 _text 段 ( 代码段 ) 中的源码顺序来帮助理解 .

在以上基础上我们来实战操作一下: 在demo中创建order文件,然后在order file中指定路径,我们在order文件中写入符号,编译后会发现此时顺序就是按照我们写入顺序来,而不是按照Build Phases中的Compile Sources顺序来的,因此剩下的任务就是拿到启动时候的方法

Clang插桩 用fishHook来hook方法 objc_msgSend来拿到其第二个参数SEL

抖音的方案存在局限和瓶颈,initialize hook不到,部分block hook不到,C++通过寄存器的间接函数调用静态扫描不出来,而使用Clang插桩的方式可以解决这个问题,做到全部hook。

  1. Other C Flags 来到 Apple Clang - Custom Compiler Flags 中 , 添加

-fsanitize-coverage=trace-pc-guard

  1. 按文档在代码中添加如下代码(我是直接加在demo中viewDidLoad中) startstop这个内存区间保存的就是工程所有符号的个数。
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.

  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

main阶段:

  1. 懒加载
  2. 发挥CPU的价值(多线程来进行初始化,根据实际情况处理)
  3. 启动阶段展示的界面用纯代码,不要用SB,因为SB还是会解析转换成代码

安装包瘦身

安装包瘦身 安装包(IPA)主要由可执行文件、资源组成

1.资源(图片、音频、视频等)
采取无损压缩
去除没有用到的资源: github.com/tinymind/LS…

2.可执行文件瘦身

编译器优化 Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES

去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions

利用AppCode(www.jetbrains.com/objc/)检测未使用… -> Code -> Inspect Code

编写LLVM插件检测出重复代码、未被调用的代码

架构

架构是软件开发中的设计方案,它是类与类之间的关系、模块与模块之间的关系、客户端与服务端的关系

常见架构:
MVC、MVP、MVVM、VIPER、CDD
三层架构、四层架构

MVC

MVC Apple版本 典型的就是tableView
view和model是不产生联系的,在VC中取到view中的属性,然后取到模型进行赋值 image.png

如下代码是在VC中,MJShop是模型数据,这种就是传统的MVC

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NewsCell" forIndexPath:indexPath];
    
    MJShop *shop = self.shopData[indexPath.row];
    
    cell.detailTextLabel.text = shop.price;
    cell.textLabel.text = shop.name;
    
    return cell;
}

MVC 变种
view和model产生联系,就是我们常用的,在vc中创建view,在view中声明一个模型属性,传递模型数据进行赋值 image.png

MVP
MVP其实和Apple版本的MVC相似,P就是充当C的角色

image.png

代码示例
将来如果有其他的view加入进来,只需要再定义一个新的Presenter即可,VC只需要管理各种Presenter就可以,其他的创建,数据操作都在Presenter中.

🌹模型
@interface MJApp : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *image;
@end


🌹自定义view  .h文件
#import <UIKit/UIKit.h>

@class MJAppView;

@protocol MJAppViewDelegate <NSObject>
@optional
- (void)appViewDidClick:(MJAppView *)appView;
@end

@interface MJAppView : UIView
- (void)setName:(NSString *)name andImage:(NSString *)image;
@property (weak, nonatomic) id<MJAppViewDelegate> delegate;
@end


🌹自定义view  .m文件
#import "MJAppView.h"

@interface MJAppView()
@property (weak, nonatomic) UIImageView *iconView;
@property (weak, nonatomic) UILabel *nameLabel;
@end

@implementation MJAppView

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        UIImageView *iconView = [[UIImageView alloc] init];
        iconView.frame = CGRectMake(0, 0, 100, 100);
        [self addSubview:iconView];
        _iconView = iconView;
        
        UILabel *nameLabel = [[UILabel alloc] init];
        nameLabel.frame = CGRectMake(0, 100, 100, 30);
        nameLabel.textAlignment = NSTextAlignmentCenter;
        [self addSubview:nameLabel];
        _nameLabel = nameLabel;
    }
    return self;
}

- (void)setName:(NSString *)name andImage:(NSString *)image
{
    _iconView.image = [UIImage imageNamed:image];
    _nameLabel.text = name;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if ([self.delegate respondsToSelector:@selector(appViewDidClick:)]) {
        [self.delegate appViewDidClick:self];
    }
}


🌹 P.h文件
@interface MJAppPresenter : NSObject
- (instancetype)initWithController:(UIViewController *)controller;


🌹 P.m文件
#import "MJAppPresenter.h"
#import "MJApp.h"
#import "MJAppView.h"

@interface MJAppPresenter() <MJAppViewDelegate>
@property (weak, nonatomic) UIViewController *controller;
@end

@implementation MJAppPresenter

- (instancetype)initWithController:(UIViewController *)controller
{
    if (self = [super init]) {
        self.controller = controller;
        
        // 创建View
        MJAppView *appView = [[MJAppView alloc] init];
        appView.frame = CGRectMake(100, 100, 100, 150);
        appView.delegate = self;
        [controller.view addSubview:appView];
        
        // 加载模型数据
        MJApp *app = [[MJApp alloc] init];
        app.name = @"QQ";
        app.image = @"QQ";
        
        // 赋值数据
        [appView setName:app.name andImage:app.image];
//        appView.iconView.image = [UIImage imageNamed:app.image];
//        appView.nameLabel.text = app.name;
    }
    return self;
}

#pragma mark - MJAppViewDelegate
- (void)appViewDidClick:(MJAppView *)appView
{
    NSLog(@"presenter 监听了 appView 的点击");
}

MVVM
在MVVM数据绑定可以用RAC,此处我们选择Facebook的KVOController

image.png

代码示例:
在VC中声明viewModel属性来拥有viewModel,viewModel提供初始化方法,将VC传递过去

🌹 VC .m文件 
#import "ViewController.h"
#import "MJAppViewModel.h"

@interface ViewController ()
@property (strong, nonatomic) MJAppViewModel *viewModel;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.viewModel = [[MJAppViewModel alloc] initWithController:self];
}

模型依然是独立存在

@interface MJApp : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *image;
@end

view当中与viewModel进行绑定

🌹 view .h文件
@class MJAppView, MJAppViewModel;

@protocol MJAppViewDelegate <NSObject>
@optional
- (void)appViewDidClick:(MJAppView *)appView;
@end

@interface MJAppView : UIView
@property (weak, nonatomic) MJAppViewModel *viewModel;
@property (weak, nonatomic) id<MJAppViewDelegate> delegate;
@end


🌹 .m 文件
#import "MJAppView.h"
#import "NSObject+FBKVOController.h"

@interface MJAppView()
@property (weak, nonatomic) UIImageView *iconView;
@property (weak, nonatomic) UILabel *nameLabel;
@end

@implementation MJAppView

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        UIImageView *iconView = [[UIImageView alloc] init];
        iconView.frame = CGRectMake(0, 0, 100, 100);
        [self addSubview:iconView];
        _iconView = iconView;
        
        UILabel *nameLabel = [[UILabel alloc] init];
        nameLabel.frame = CGRectMake(0, 100, 100, 30);
        nameLabel.textAlignment = NSTextAlignmentCenter;
        [self addSubview:nameLabel];
        _nameLabel = nameLabel;
    }
    return self;
}

- (void)setViewModel:(MJAppViewModel *)viewModel
{
    _viewModel = viewModel;
    
    __weak typeof(self) waekSelf = self;
    [self.KVOController observe:viewModel keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        waekSelf.nameLabel.text = change[NSKeyValueChangeNewKey];
    }];
    
    [self.KVOController observe:viewModel keyPath:@"image" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        waekSelf.iconView.image = [UIImage imageNamed:change[NSKeyValueChangeNewKey]];
    }];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if ([self.delegate respondsToSelector:@selector(appViewDidClick:)]) {
        [self.delegate appViewDidClick:self];
    }
}

viewModel

🌹 .h 文件
@interface MJAppViewModel : NSObject
- (instancetype)initWithController:(UIViewController *)controller;
@end


.m文件
#import "MJAppViewModel.h"
#import "MJApp.h"
#import "MJAppView.h"

@interface MJAppViewModel() <MJAppViewDelegate>
@property (weak, nonatomic) UIViewController *controller;
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *image;
@end

@implementation MJAppViewModel

- (instancetype)initWithController:(UIViewController *)controller
{
    if (self = [super init]) {
        self.controller = controller;
        
        // 创建View
        MJAppView *appView = [[MJAppView alloc] init];
        appView.frame = CGRectMake(100, 100, 100, 150);
        appView.delegate = self;
        appView.viewModel = self;
        [controller.view addSubview:appView];
        
        // 加载模型数据
        MJApp *app = [[MJApp alloc] init];
        app.name = @"QQ";
        app.image = @"QQ";
        
        // 设置数据
        self.name = app.name;
        self.image = app.image;
    }
    return self;
}

#pragma mark - MJAppViewDelegate
- (void)appViewDidClick:(MJAppView *)appView
{
    NSLog(@"viewModel 监听了 appView 的点击");
}

三层或者四层架构

image.png

相关文章:
iOS 保持界面流畅的技巧
抖音二进制重排