卡顿优化
CPU和GPU
CPU(Central Processing Unit,中央处理器):
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
GPU(Graphics Processing Unit,图形处理器):
纹理的渲染
整个流程图:
注意:在iOS中是双缓冲机制,有前帧缓存、后帧缓存
屏幕成像原理
卡顿原因
解决卡顿的思路:
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.尽量避免出现离屏渲染
关于离屏渲染
卡顿检测 LXDAppFluecyMonitor 通过向Runloop注册observer
耗电优化
耗电
1.CPU处理,Processing
2.网络,Networking
3.定位,Location
4.图像,Graphics
耗电优化
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方法中执行
dyld阶段
runtime阶段
App启动 -main阶段
注意:在main函数阶段我们可以使用第三方的打点工具BLStopwatch来查看代码的执行时间
启动优化
pre_main阶段
二进制重排(源自抖音团队)
关于虚拟内存和物理内存?
操作:
- 使用Instrument中的system trace,将应用跑起来
- 查看虚拟内存中的File Backed Page In中的count,代表
page fault的次数,下图是159次(注意:iOS中一页16k)
操作:二进制重排是可以用xcode来进行配置的
我们以objc750源码为例子,在xcode的Build Setting中查找order file,最终会在路径下生成libobjc.order的文件,里面存储的都是符号文件,见下图,苹果编译的动态库,就是按照order来进行排列的,也就是说二进制重排苹果一直是有使用到的
同理,我们自己也可以配置一个order文件路径,在这个 order 文件中,将你需要的符号按顺序写在里面,而当当工程 build 的时候 , Xcode 会读取这个文件 , 打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O
那么如何查看我们工程中的符号顺序呢?
在Build Setting中搜索
Write Link Map File,将其设置为YES,意思是替我们生成一个符号文件,我们点击Products中的.App,然后show in finder,最终找到它。
头部告诉我们具体链接了哪些.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
注意:
上述文件中最左侧地址就是 实际代码地址而并非符号地址 , 因此我们二进制重排并非只是修改符号地址 , 而是利用符号顺序 , 重新排列整个代码在文件的偏移地址 , 将启动需要加载的方法地址放到前面内存页中 , 以此达到减少 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。
- Other C Flags 来到 Apple Clang - Custom Compiler Flags 中 , 添加
-fsanitize-coverage=trace-pc-guard
- 按文档在代码中添加如下代码(我是直接加在demo中viewDidLoad中)
start和stop这个内存区间保存的就是工程所有符号的个数。
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阶段:
- 懒加载
- 发挥CPU的价值(多线程来进行初始化,根据实际情况处理)
- 启动阶段展示的界面用纯代码,不要用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中的属性,然后取到模型进行赋值
如下代码是在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中声明一个模型属性,传递模型数据进行赋值
MVP
MVP其实和Apple版本的MVC相似,P就是充当C的角色
代码示例
将来如果有其他的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
代码示例:
在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 的点击");
}
三层或者四层架构
相关文章:
iOS 保持界面流畅的技巧
抖音二进制重排