物理内存&虚拟内存
- 物理内存
- 在进程运行时,为操作系统及进程提供临时存储空间,需要借助物理存储器进行存储
- 虚拟内存
- Virtual Memory,是计算机内存管理的技术之一,为每个进程提供一个连续并私有的地址空间,从而保护每个进程的地址空间不被其它进程损坏,降低了开发的复杂度,也确保了程序执行的安全性
内存分页
为了方便虚拟内存
和物理内存
的映射及管理,目前大部分计算机的内存都采用了分页管理,将虚拟内存和物理内存分别分割成大小相同的单位。iOS下每个进程空间先分段,每个段内再分页,所以物理地址由 段号 + 段内页号 + 页内地址
组成,目前 64 位系统每页为 16KB。
内存分页
的最大意义在于支持了物理内存的离散使用,连续的虚拟内存页实际映射的物理内存页可以是任意存放的,方便了操作系统对物理内存的管理,也能最大化利用物理内存,减少内存空间的浪费。
虚拟寻址
CPU 访问虚拟地址,经过 MMU(Memory Management Unit)翻译成物理地址访问内存,虽然效率相对于物理 寻址有所下降,但如之前所说,CPU 的效率是远远高于其对内存的读写,所以影响不大
缺页处理
当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次缺页中断(Page Fault)
,然后去分配物理内存,有需要的话会从磁盘mmap读入数据。
内存交换机制(Swap In/Out )
通常磁盘中都会有一个区域作为内存交换空间(Swap Space),内存管理单元(MMU)会将内存中暂时不用的内容写到交换空间中,该过程即为 Swap Out;
当需要使用访问时,在从内存交换空间中读取,即 Swap In;这两种操作都是比较耗时的,频繁使用对性能影响较大,通常在物理内存不足以供程序使用时,操作系统会自动进行 Swap 操作。
iOS 不支持该机制,由于手机使用的都是闪存,频繁读写对其影响较大,并且闪存的空间也较小,目前最高配置为 512 GB,跟电脑相比还是有不小差距,因此 iOS 采用的是内存压缩机制(Compressed memoryy)
内存压缩机制
当物理内存不够用时,iOS 会按照 LRU(Least Recently Used)最近最少使用原则,将部分物理内存的内容压缩,在需要读写时再解压,从而达到节约内存的效果。
在其他操作系统下(非 iOS),还可以用压缩的内存进行内存和磁盘数据交换,提升交换效率。将交换后的压缩内容放在内存里,发生 page fault 时再解压缩出来,那么从时间上,将只有CPU的压缩和解压缩的开销,而没有耗时多的 I/O 传输的开销。当然代价就是,从空间上,需要占用一部分的内存资源。
iOS程序的内存布局
iOS程序当中,内存布局分为保留区、代码段、数据段、堆区、栈区以及内核区 如:
// 已初始化的全局变量
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";
// 堆区,通过alloc、malloc、calloc分配的空间,分配的地址越来越大
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]));
}
}
输出结果:
使用CADisplayLink、NSTimer有什么注意点?
CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
比如:
//在viewcontroller中持有timer,而timer的target又对viewcontroller产生强引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTest)
userInfo:nil repeats:YES];
解决方案:
1.使用block
__weak typeof(self) weakSelf = self;
[NSTimer timerWithTimeInterval:3.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf dosomething];
}];
2.使用中间人消息转发机制
/*---------ProxyTarget.h 文件---------*/
// 继承自NSProxy而不是NSObject,可以使消息转发时效率更高,
// 因为没有在类对象、元类对象中查找实例方法、类方法等
@interface ProxyTarget : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
// weak指向传递过来的target
@property (weak, nonatomic) id target;
@end
/*---------ProxyTarget.m 文件---------*/
#import "ProxyTarget.h"
@implementation ProxyTarget
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
ProxyTarget *proxy = [ProxyTarget alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
// 将消息转发给目标target
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
viewcontroller调用方式
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
// 在viewcontroller内保留timerTest实现方法,消息中间人会将方法转发过来
- (void)timerTest
{
NSLog(@"%s", __func__);
}
3.使用category方式添加Target-Action
@implementation NSTimer (BlcokTimer)
+ (NSTimer *)bl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(bl_blockSelector:) userInfo:[block copy] repeats:repeats];
}
+ (void)bl_blockSelector:(NSTimer *)timer {
void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
通过block的方式,获取action,实际的target设置为self,即NSTimer类。这样我们在使用timer时,由于target的改变,就不再有循环引用了
CADisplayLink、NSTimer和GCD定时器特点,哪个更加准时?
CADisplayLink、NSTimer依赖于RunLoop,如果RunLoop执行过程比较繁重,会导致不准时(可能会略大于执行时间),而GCD是不依赖于RunLoop的,所以执行时间上更加准时
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间(start是几秒后开始执行,interval是时间间隔)
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
});
// 启动定时器
dispatch_resume(timer);
Tagged Pointer
- 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
- 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
- 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
- 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
- 当调用方法时,objc_msgSend能识别Tagged Pointer,比如调用NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
- 如何判断一个指针是否为Tagged Pointer?iOS平台,最高有效位是1(第64bit), Mac平台,最低有效位是1
测试代码:
// n1、n2和n3这三种写法最终是等价的,编译器最终会解释成numberWithInt
// 由于存储的是小对象,在内存地址值中的表现会是0xData+Tag形式,
NSNumber *n1 = [NSNumber numberWithInt:10];
NSNumber *n2 = @(10);
NSNumber *n3 = @10;
// 而这个存储的是一个大对象,最终n4的指针存储的是堆中的NSNumber对象的地址值
NSNumber *n4 = @(0xfffffffffffff);
举个例子:
// 对viewcontroller里的name(strong, nonatomic)属性进行set
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//- (void)setName:(NSString *)name
//{
// if (_name != name) {
// [_name release];
// _name = [name retain];
// }
//}
// 这种会crash,由于对个线程同时对name进行set,
// 而set方法具体表现形式是如果旧值和新值不相等,先将旧值release掉,然后在对旧值赋值并retain
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
}
// 这种不会crash,由于字符串abc比较小,所以在内存地址中就直接将abc和Tag存储进去了,在运行时就直接从内存地址中获取到abc了
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"abc"];
// 打印结果:__NSCFString NSTaggedPointerString
NSLog(@"%@ %@", [str1 class], [str2 class]);
Runtime源码:
copy 和 mutableCopy
拷贝的目的:产生一个副本对象,跟源对象互不影响,修改了源对象不会影响副本对象; 修改了副本对象,不会影响源对象
iOS提供了2个拷贝方法
copy
,不可变拷贝,产生不可变副本
NSMutableCopy
,可变拷贝,产生可变副本
NSString
的copy
与mutableCopy
NSString *str1 = [NSString stringWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 返回的是NSString,不可变副本,str2的地址跟str1的地址一样,本质是上将str1进行了retain操作
NSMutableString *str3 = [str1 mutableCopy]; // 返回的是NSMutableString,可变副本,str3的地址跟str1不一样
// 可以调用str3的appendString方法正常拼接字符串(因为mutableCopy返回的是可变副本),
但如果强行用NSMutableString接收str2,同样进行appendString方法,
运行起来就会报错(因为copy返回的是不可变副本)
NSMutableString
的copy
与mutableCopy
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 深拷贝(产生了一个新的不可变的副本对象,str2和str1两个指针地址不一样)
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝,str3可变
深拷贝和浅拷贝
1.深拷贝:内容拷贝,产生新的对象
2.浅拷贝:指针拷贝,没有产生新的对象
本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.