iOS内存管理

335 阅读17分钟

定时器

CADisplayLink 定时器

1,调用频率和屏幕的刷帧频率一致,60FPS 2,监控屏幕是否掉帧。(cpu计算,gpu绘制,如果处理不了就会掉帧)

NSTimer

初始化方法

scheduledTimerWithTimeInterval 直接放入到了runloop 中, timerWithTimeInterval 只初始化没有让如到runloop 中

timer 存在的循环引用

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

1,self 拥有了 timer ,timer又拥有了self,导致循环引用

timer 循环引用的解决(错误方法)

错误方法,__weak 修饰self, 原因1 __weak 修饰block 内的self 时,block对内部的self弱引用,打破了循环,但是这个不是block. 原因2 用__weak 修饰self,timer内部可能是用强引用持有的self,还是没有打破循环引用。

timer 循环引用的解决(错误方法)

1,用带block的timer,__weak修饰self (即可打破循环引用)

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];

timer 循环引用的解决(加入中间层打破循环引用)

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];

1,timer强持有中间层,中间层弱持有控制器(此处为代指) 2,中间层没有处理事件的能力(timerTest),消息发送的本质解决问题 交给传入的弱持有对象,即可。

@implementation MJProxy

+ (instancetype)proxyWithTarget:(id)target
{
    MJProxy *proxy = [[MJProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}

@end

timer 循环引用的解决(加入NSProxy中间层打破循环引用)

1,NSProxy 和NSObject平级,专门用来做消息转发的(不会去父类中寻找,有没有实现此方法) 2,因此它的效率更高

@implementation MJProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    MJProxy *proxy = [MJProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end

GCD定时器 (NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时)

而GCD的定时器会更加准时

GCD定时器的使用

gcd 定时器的封装

1,设计接口,外界如何调用。 2,考虑多线程,(当多个线程都创建定时器时)

信号量为1,同一时间只执行一个线程里面的任务

    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
任务
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);

iOS中的内存布局(代码段,数据段(先常量后变量),堆,栈,内核)

1,堆空间是越来越大的。 2,栈空间是越来越小的。

int a = 10;
int b;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        static int c = 20;
        
        static int d;
        
        int e;
        int f = 20;

        NSString *str = @"123";
        
        NSObject *obj = [[NSObject alloc] init];
        
        NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
              &a, &b, &c, &d, &e, &f, str, obj);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

/*
 字符串常量
 str=0x10dfa0068
 
 已初始化的全局变量、静态变量
 &a =0x10dfa0db8
 &c =0x10dfa0dbc
 
 未初始化的全局变量、静态变量
 &d =0x10dfa0e80
 &b =0x10dfa0e84
 
 堆
 obj=0x608000012210
 
 栈
 &f =0x7ffee1c60fe0
 &e =0x7ffee1c60fe4
 */

Tagged Pointer (将小数据直接存到指针里面)

1,从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

2,在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

3,使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

4,当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

5,objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

6,如何判断一个指针是否为Tagged Pointer? iOS平台,最高有效位是1(第64bit) Mac平台,最低有效位是1 这里是判断的源码

Tagged Pointer (不是OC对象)

1,第一段代码会崩溃,报坏内存访问。

因为线程不安全。ser方法时,name可能已经被释放了

set方法的本质
- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

2,第二段代码不会崩溃

本质上就不会走set方法。

MRC

改成mrc 模式

Automatic Reference congting 改为No

引用计数控制对象的生命周期

一个对象的引用计数为0时销毁内存,防止内存泄漏

MRC 手动管理内存,当有alloc 时就得有release

当只有一个对象时,

一个alloc 对应一个release

当一个对象还持有另外一对象时

要让当前对象强只有另一个对象,让另一个对象的引用计数+1。

防止另一个对象被销毁了,这个对象还要调用。

对象持有另一个对象的set方法

  1. 如果传入的新对象和旧对象一样,就什么都不做(为什么什么都不做,因为释放的时候也是释放一次,这样管理更方便,如果愿意增加几次释放几次也可以,但是太不简洁了)
  2. 如果传入的不一样,先释放旧的(对于旧对象,持有的时候+1,现在不用了-1)
  3. 传的新的对象,管理其引用计数。开始接管其生命周期。
- (void)setDog:(MJDog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog retain];
    }
}

内存管理原则,谁创建谁销毁

1,一个对象持有另一个对象时,另一个对象引用计数+1,此对象也要负责另外一个对象的释放。(一般本身销毁的时候,释放自己管理的对象。)

2,当一个对象变更属性对象时,先释放旧对象,在接管新对象。

编译器带来的优化

.h文件中声明的属性只是申明的set和get方法,但是没有方法的实现和_成员变量。

@synthesize 关键字可以完成生成成员变量和属性的setter、getter实现。 但是现在,这个关键字变成默认的了,不用主动声明。

类方法创建对象

类方法创建的对象,都放入自动释放池了。

+ (instancetype)person
{
    return [[[self alloc] init] autorelease];
}

ARC 都帮我们做了什么?

LLVM + Runtime

  1. LLVM (加上,release,retain,autoRlease 操作)
  2. Runtime (运行时,清空弱引用的对象)

copy

为什么要有copy (产生一个副本对象,跟源对象互不影响)

修改了源对象,不会影响副本对象

修改了副本对象,不会影响源对象

两种copy 方法 (copy 一定会产生副本对象,为了和源对象互不影响,但是不一定产生新的对象)

通过打印地址来看有没有生成新的对象。

copy

copy,不可变拷贝,产生不可变副本(可能不会产生新对象) 如果源对象本身不可变,copy之后产生不可变副本,此时只需要产生一个新的指针指向源地址就可以了。(都是不可变对象,没必要新开辟内存了)

mutableCopy (只有用于Foundation中的对象)

mutableCopy,可变拷贝,产生可变副本(一定会产生新对象) 它只生成新对象,才会产生可变副本。

如果mutableCopy 可变数组也可能浅copy(可变集合中元素是自定义(自定义对象要准守协议)对象或者二维集合)

两种copy结果(深copy和浅copy)

深拷贝

浅拷贝

1.深拷贝:内容拷贝,产生新的对象 2.浅拷贝:指针拷贝,没有产生新的对象

为什么用copy修饰字符串

1,如果用storng修饰的话,源字符串修改。新的字符串也会修改。 2,UITextField的text是用copy修饰的,防止拿着这个属性去拼接别的字符串,让这个text只接受生成好的字符串。

自定义对象的copy

1,准守 协议 2,重写copyWithZone 方法

- (id)copyWithZone:(NSZone *)zone
{
    MJPerson *person = [[MJPerson allocWithZone:zone] init];
    person.age = self.age;
//    person.weight = self.weight;
    return person;
}

weak 内部实现(重点)

自动释放池 autoreleasepool 的原理

autoreleasepool 转成C++代码 1,构造函数(创建函数时)(push 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址 2,析构函数(销毁函数时)(调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY)

 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };

自动释放池的主要底层数据结构 :__AtAutoreleasePool、AutoreleasePoolPage

AutoreleasePoolPage的结构

  1. 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址(即放入自动释放池的对象)
  2. 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

  1. 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

  2. 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

  3. id *next指向了下一个能存放autorelease对象地址的区域

autoreleasepool 什么时候释放

1,肯定不是被main函数外边的autorele释放的(当然,main也不是在程序退出的时候才释放)

autoreleasepool 和runloop

这个对象什么时候调用release,是由RunLoop来控制的。 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release

原理 iOS在主线程的Runloop中注册了2个Observer 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush() 第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

方法里有局部对象, 出了方法后会立即释放吗

如果这个对象是通过autoreleasepool的方法,不会立即释放。 如果是arc 自动管理的话会立即释放。

可以通过以下私有函数来查看自动释放池的情况

c语言语法,声明一下可以直接调用

extern void _objc_autoreleasePoolPrint(void);

什么时候手动创建自动释放池

for 循环创建大量零时变量的时候。此时自动释放会有延时。需要手动创建

性能优化

CPU和GPU

CPU(Central Processing Unit,中央处理器) 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)

GPU(Graphics Processing Unit,图形处理器) 纹理的渲染

卡顿产生的原因

CPU、GPU资源消耗过大

卡顿问题的解决

卡顿解决的主要思路 尽可能减少CPU、GPU资源消耗

按照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. 尽量避免出现离屏渲染

卡顿优化

耗电的主要来源

CPU处理,Processing

网络,Networking

定位,Location

图像,Graphics

耗电优化

  1. 尽可能降低CPU、GPU功耗

  2. 少用定时器

  3. 优化I/O操作 尽量不要频繁写入小数据,最好批量一次性写入 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问 数据量比较大的,建议使用数据库(比如SQLite、CoreData)

  4. 网络优化 减少、压缩网络数据 如果多次请求的结果是相同的,尽量使用缓存 使用断点续传,否则网络不稳定时可能多次传输相同的内容 网络不可用时,不要尝试执行网络请求 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载

  5. 定位优化 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新 尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:

  6. 硬件检测优化 用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

APP的启动

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

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

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

APP的启动阶段

APP的冷启动可以概括为3大阶段 dyld runtime main

APP的启动 - dyld

  1. dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

  2. 启动APP时,dyld所做的事情有 装载APP的可执行文件,同时会递归加载所有依赖的动态库 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理

APP的启动 - runtime

1.启动APP时,runtime所做的事情有 调用map_images进行可执行文件内容的解析和处理 在load_images中调用call_load_methods,调用所有Class和Category的+load方法 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等) 调用C++静态初始化器和__attribute__((constructor))修饰的函数

  1. 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理

APP的启动 - main

总结一下

  1. APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库

  2. 并由runtime负责加载成objc定义的结构

  3. 所有初始化工作结束后,dyld就会调用main函数

  4. 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

安装包优化

安装包(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

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

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

可执行文件瘦身 编译器优化

  1. 生成LinkMap文件,可以查看可执行文件的具体组成

  2. 可借助第三方工具解析LinkMap文件: github.com/huanxsd/Lin…

架构模式

MVC

优点:View、Model可以重复利用,可以独立使用 缺点:Controller的代码过于臃肿

MVC变种

优点:对Controller进行瘦身,将View内部的细节封装起来了,外界不知道View内部的具体实现 缺点:View依赖于Model

MVP

Model-View-Presenter

MVVM

Model-View-ViewModel

封层架构

设计模式

什么是设计模式

是一套被反复使用、代码设计经验的总结 使用设计模式的好处是:可重用代码、让代码更容易被他人理解、保证代码可靠性 一般与编程语言无关,是一套比较成熟的编程思想

设计模式分类

创建型模式:对象实例化的模式,用于解耦对象的实例化过程 单例模式、工厂方法模式,等等

结构型模式:把类或对象结合在一起形成一个更大的结构 代理模式、适配器模式、组合模式、装饰模式,等等

行为型模式:类或对象之间如何交互,及划分责任和算法 观察者模式、命令模式、责任链模式,等等

小结

定时器的类型

界面刷新,timer(永不用加入到runloop中)

定时器的循环引用的解决方案。

1,__weak 修饰timer中的block 对象(只使用带block的timer) 2,添加中间层打破循环引用,再利用消息发送机制,让中间层处理timer要执行的方法。

内存布局(内存地址由低到高)

1,代码段:编译后的代码

2,常量

3,变量

4,堆

5,栈

Tagged Pointer (将小数据直接存到指针里面)

如何判断不是Tagged Pointer

打印地址的最后一位,转成二进制后判断 iOS平台,最高有效位是1(第64bit) Mac平台,最低有效位是1

MRC

创建和释放要一一对应。

谁持有谁释放(set方法的三步)

1,判断有没有变更对象,2释放旧的对象,3接管新的对象。

copy

copy 和mutableCopy 是操作方法

1.copy,不可变拷贝,产生不可变副本 2.mutableCopy,可变拷贝,产生可变副本

深copy 和浅copy是操作结果

1.深拷贝:内容拷贝,产生新的对象 2.浅拷贝:指针拷贝,没有产生新的对象

weak的内部实现结构

自动释放池 autoreleasepool (实现原理)

自动释放池 autoreleasepool 和 runloop (依赖关系)

性能优化

App 三大启动阶段

  1. APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库

  2. 并由runtime负责加载成objc定义的结构

  3. 所有初始化工作结束后,dyld就会调用main函数

GPU CPU

安装包优化

  1. 资源(图片、音频、视频等)
  2. 可执行文件瘦身

架构模式

MVC,MVP,MVVM

常见的设计模式