iOS App性能优化


Apple Developer Documentations 里搜索 Performance Overview有相关性能优化的文档

一、内存5大区

运行时分配:

  • 栈区
    栈区地址从高到低,连续分配。运行时分配。
  • 堆区
    堆区手动分配内存,地址不连续,堆区反应速度没有栈区快。运行时分配。

编译时分配:

  • 静态区(BSS段)
    未初始化的全局变量和静态变量。编译时分配。
  • 常量区(数据段)
    已初始化的全局变量和静态变量。编译时分配。
  • 程序代码区
    代码的存储区域,编译时分配。

二、引用计数

  • 如果对象使用了TaggedPointer,苹果会直接将其指针值作为引用计数返回。
  • 引用计数可以直接存储在优化过的isa指针中。
  • 如果isa指针存储不下,引用计数就会把一部分存储在一个散列表中。

TaggedPoint

  • 专门用来存储小对象,例如NSNumber、NSDate和NSString的stringWithFormat初始化的比较短的字符串等。
  • 指针的值不再是地址,而是真正的值。所以,实际上它不再是一个对象了,它只是披着对象皮的普通变量而已!所以,他的内存并不存储在堆中,而是存在栈中,也不需要malloc和free。
  • 在内存读,取速度快。

isa_t

早期isa_t是一个指向Class的isa指针。这个指针经过优化,目前为一个包含位域的联合体。
isa_t的标识位has_sidetable_rc标识是否存储在sidetable(散列表)里。如果没有会用isa_t的"19位"(位域后面的数字标识占多少位)对引用计数rc(retain count)进行存储。

散列表(SideTable)存储原理

SideTable是一个包含SideTable对象的结构体;
SideTable散列表主要实现类似NSDictionary的Key,Value存储,在取值的时候不需要遍历,只需要根据Key,根据相应的算法算出一个对应index直接找到相应的位所存储的数据。(空间换时间)
OC里SideTable通过sidetable_getExtraRC_nolock()获取存储在散列表中的引用计数。

Weak实现原理

1、弱引用对象,底层也是使用了哈希存储,或者叫散列存储,以对象的内存地址为key,指向该对象的所有弱引用的指针的个数作为值。
2、释放时,就是以对象的内存地址作为key,去存储弱引用对象的哈希表里,找到所有的弱引用对象,然后设置为nil,最后移除这个弱引用的散列表。

三、自动释放池

简单原理

  • autorelease 延迟调用release。通过 clang -rewrite-objc filename.m 查看最简单的main程序下生产的filename.cpp
@autoreleasepool 		//__AtAutoreleasePool __autoreleasepool;
{						//objc_autoreleasePoolPush();
}						//objc_autoreleasePoolPop(atqutoreleasepoolobj);
  • __AtAutoreleasePool的方法定义由类AutoreleasePoolPage管理。

  • 自动释放池是由AutoreleasePoolPage以双向链表的形式连接起来的。

自动释放池的主要实现

  • 在MRC下通过打印函数,查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);//打印函数
  • AutoreleasePoolPage可存储的对象个数:
    AutoreleasePoolPage每页4094字节(4M)。AutoreleasePoolPage自身成员变量占56个字节,用于指针管理时的偏移量。4040可以存储505个对象,objc_autoreleasePoolPush()时生产一个POOL_BOUNDARY分隔对象,所以AutoreleasePoolPage可以存储504个延迟释放对象
  • objc_autoreleasePoolPush(id obj)操作实现:***
static inline void *push()  {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
}

在DebugPoolAllocation开启状态下,调用autoreleaseNewPage(id obj)生成一个新的AutoreleasePoolPage;否则调用autoreleaseFast(id obj),把当前对象加入当前活跃度AutoreleasePoolPage;

  • autoreleaseNewPage(id obj)操作实现:
    获取当前活跃的AutoreleasePoolPage,不为空则调用autoreleaseFullPage(id obj, AutoreleasePoolPage page)添加,否则调用autoreleaseNoPage()生成一个AutoreleasePoolPage并返回。
  • autoreleaseFullPage(id obj, AutoreleasePoolPage page)操作实现:
    通过page->child判断当前AutoreleasePoolPage是否已满(page->child为空),如果未满把自动释放对象加入当前AutoreleasePoolPage,如果已满重新创建一个新的AutoreleasePoolPage;
  • autoreleaseFast(id obj)操作实现:
    获取当前活跃的AutoreleasePoolPage,如果有并且未满,把当前对象加入当前活跃度AutoreleasePoolPage;如果已满调用autoreleaseFullPage(id obj, AutoreleasePoolPage page)操作;否则调用autoreleaseNoPage()操作创建一个新的AutoreleasePoolPage并设置为活跃。
  • objc_autoreleasePoolPop(void *token)实现***
    根据传进来的token获取当前操作的AutoreleasePoolPage页,调用releaseUntil(id stop)释放AutoreleasePoolPage所存储的延迟释放对象。

总结及参考:

1、自动释放池是由AutoreleasePoolPage以双向链表的方式实现的;
2、当对象调用autorelease()方法,会将延迟释放对象加入AutoreleasePoolPage中;加入过程首先调用objc_autoreleasePoolPush(id obj)获取当前活跃的AutoreleasePoolPage,如果没有就创建一个全新的AutoreleasePoolPage,并记录标示位。
3、调用objc_autoreleasePoolPop(void *token),会向AutoreleasePoolPage栈中的对象发送release消息。

四、引起内存泄漏的原因

1、强引用

使用_weak打破循环引用环

简单的做个小结:

  • 1、在使用block时,如果block内部需要访问self的方法、属性、或者实例变量应当使用weakSelf
  • 2、如果在block内需要多次访问self,则需要使用strongSelf
  • 3、如果在block内部存在多线程环境访问self,则需要使用strongSelf
  • 4、block本身不存在多线程之分,block执行是否是多线程,取决于当前的持有者是否是以多线程的方式来调用它。

2、非OC对象,没有手动释放

CF类、CG类记得手动释放,或关闭

3、循环引用

ClassA *a = [ClassA new];
ClassB *b =  [ClassB new];
a.b = b;
b.a = a;

Block循环引用

通过终端命令:clang -rewrite-objc *.m可以看到便于后的*.cpp(c++文件)。
可以看到block结构体,捕获外部变量直接做为成员变量,如果是weak变量很可能还未使用,就被释放,所以通常用一个block里的strong变量指向外面的weak变量,这样保证代码块执行时,捕获的变量未被释放。

__weak typeof(self) weakSelf = self;
__strong typeof(self) strongSelf = self;

NSTimer循环引用

RunLoop->timer->self

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

这个NSTimer的类方法生成的对象timer对象被加到当前的runloop当中,并且持有了aTarget,相当于

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

所以只要程序不被释放,就存在一个timer抢持有aTarget

  • 办法一
- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent==nil) {
        [_timer invalidate];
        _timer = nil;
    }
}
  • 方法二(借用Target)
    RunLoop->timer->Target->self
 self.timerTarget = [NSObject new];
 class_addMethod([_timerTarget class], @selector(fire), (IMP)dynamicFire, "v@:");
 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];

或者
RunLoop->timer->Target<-self

self.timerTarget = [NSObject new];
Method m = class_getInstanceMethod([self class], @selector(fire));
 class_addMethod([_timerTarget class], @selector(fire), method_getImplementation(m), "v@:");
  • 方法三(NSProxy消息转发)
    RunLoop->timer->Target---->self
//重写获取方法签名
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available"){
    return [self.target methodSignatureForSelector:sel];
}
//转发给转发类的target
- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}

五、内存问题检测方法

循环引用

以上已经介绍过了。

野指针

EXC_BAD_ECCESS,勾选Xcode->Product->Scheme->Edit Scheme->Diagnostics->Momory Management->zombie Object可以检测到具体代码

内存泄漏监测方法

  • 1 、静态分析(Shift+Command+B)
    EXC_BAD_ECCESS,勾选Xcode->Product->Analyze可以检到。
  • 2 、动态监测方法
    Instruments(Xcode->Product->Profile)或第三方监测工具。具体使用方法可用参照Instruments->Help->Instruments Help
  • 3、dealloc

内存检测工具案例(MLeakFinder)

pod 'MLeaksFinder'

只能监测UIView和UIViewController的内存泄漏情况:

  • 检测UIViewController是否被释放
  • hook UIViewController方法跟踪pop/push
- (void)viewWillAppear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated
  • 并且延时2秒发送相关消息,查看self调用情况。
    下载demo

六、优化建议及应用瘦身

优化建议

1、启动

  • 启动分为两种:冷启动和热启动
  • 启动的时间分为两部分:main函数执行之前、main函数至应用启动完成
    main函数之前:
  • 减少动态库、合并一些动态库。关于动态库对App启动时间影响实测,启动前的时间测量可以在Xcode->Product->Scheme->Edit Scheme->Arguments->Environment Variables设置环境变量
    DYLD_PRINT_STATISTICS 的值为1。
  • 减少Objc类、分类的数量、减少Selector数量
    main函数至应用启动完成:
  • 耗时操作,不要放在finishLaunching方法中

2、界面

卡顿的原理: 复杂页面(如含有图片、富文本的列表)的拖拽、拖动操作可能导致界面掉帧,从而导致界面卡顿;

界面流畅度的评测: Instructments里有个Core Animation工具可进行真机的界面流畅度的评测。代码工具有YYFPSLabel。帧速率单位FPS,表示每秒显示的画面帧数。如果在50~60FPS是比较流畅的。

优化建议

  • 耗时操作,不要放在主线程
  • 合理使用CPU与GPU,比如富文本用YYLabel,需要加载的UIImageView可以用SDWebViewImage

CPU: 计算显示内容, 比如视图的创建、布局计算、图片解码、文本绘制 GPU :会把CPU计算好的数据进行渲染。

3、耗能优化

在这里插入图片描述
网络优化:

1)资源优化基本就是尽可能的缩小传输数据的大小

2)可以使用ProtocolBuffer代替Json进行数据传输
Protocolbuffer(简称Protobuf或PB)是由Google推出的一种数据交换格式,它独立于语言,独立于平台。

ProtocolBuffer代替Json进行数据传输,因为ProtocolBuffer数据比Json更小,也是跨平台的,序列号与反序列化也很简单。在实际项目中,当数据变小的时候会显著提高传输速度。

应用瘦身

在这里插入图片描述
如何获得Link Map文件?
1.在XCode中开启编译选项Write Link Map File \n
XCode -> Project -> Build Settings -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置

2.工程编译完成后,在编译目录里找到Link Map文件(txt类型) 默认的文件地址:

~/Library/Developer/Xcode/DerivedData/XXX-xxxxxx/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

群野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值