Weex

Weex

一、集成 Weex 到已有应用

集成 Weex 到已有应用(官网)

二、创建Weex项目

创建Weex项目方法:

  • ①、在终端执行weex create 目录即可。

    ②、cd 到项目/platforms/platforms.json所在的platforms目录下执行sudo npm run pack:ios(如果不是在该目录会提示您platforms.json的错误)

  • 在使用前,请使用weex -v进行weex环境检测。详见【附1】Weex开发环境检测

三、Weex编码

  • 编写.we/vue代码

  • 将.we/vue代码文件转为js文件放入项目中

1、将.we/vue文件转换成 js文件的终端命令

.we/vue 转换成 js文件的终端命令 作用
weex compile dir js 会将dir文件夹下的所有we文件转换到js文件夹下
weex compile dir/xxx.we js 会将dir文件夹下的xxx.we文件转换为js文件存到js文件夹下
weex compile dir/xxx.vue js 会将dir文件夹下的xxx.vue文件转换为js文件存到js文件夹下

2、vue文件编写

【附1】Weex开发环境检测

【附2】Weex工具WebStorm的安装

【附1】Weex开发环境检测

通过执行显示当前weex版本的weex -v命令,验证Weex开发环境是否都具备了,如果都成功的时候,其会如下图所示:

image-20181217175137799

安装命令

1
2
3
4
5
6
// 国外可使用方法:
npm install -g weex-toolkit

// 国内请使用方法,否则weekpack安装不上,导致weex create命令执行出错:
sudo npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm install -g weex-toolkit

weex开发环境安装的命令对比

使用npm 使用cnpm
weex-bulider 安装成功 安装成功
weex-previewer 安装成功 安装成功
weexpack
(若无会导致执行weex create失败)
安装失败 安装成功

更详细的weex安装请查看

通常,安装了 Node.js 环境,npm 包管理工具也随之安装了。npm 是一个 JavaScript 包管理工具,它可以让开发者轻松共享和重用代码。

遇到的问题1、weex -v命令(显示当前weex版本)一直报错

解决办法:通过终端重新执行npm install -g weex-toolkit命令来重新安装weex

遇到的问题2、weex create 项目名命令(创建weex项目)出错

此问题,是你没正确安装weex-toolkit导致,详细看下个问题npm install -g weex-toolkit命令安装过程有错

遇到的问题3、npm install -g weex-toolkit命令安装过程有错

错误如下:

image-20181217173509332

分析可得错误原因是:npm的安装权限不足。

加上sudo后,执行sudo npm install -g weex-toolkit依旧如此。

最终通过进入/usr/local/lib/node_modules删除之前已经安装的weex-toolkit后,然后执行sudo npm install -g weex-toolkit安装通过。此时你会发现安装过程还有一些错误提示

image-20181217175640331

此问题引起的连锁反应是①终端执行weex -v的结果只有weex-bulider和weex-previewer,没有weexpack,从而导致②终端执行weex create 项目名失败

image-20181217174116400

【附2】Weex工具WebStorm的安装

1、WebStorm软件安装&破解

2、WebStorm配置vue环境

3、WebStorm其他配置

旧电脑(Mac)数据清理

旧电脑(Mac)数据清理

[TOC]

清理项

系统偏好设置-用户与群组,左下角解锁点击加号添加一个新的管理员账号 点击左上角苹果图标,注销登陆新账号 同一个位置选择之前的账号点击减号删除 删除的时候选择删除文件就可以了

  • AppleID(涉及appstore)

    • 系统设置里退出
    • appStore –> 商店 –> 退出登录
  • iCloud账号(涉及备忘录、提醒事项、Safari等)

  • 设置 –> iCloud
  • 邮件、备忘录、提醒事项 清理

    • [ ] 清理系统邮箱内容

      1
      2
      进入 ~/Library/Containers/com.apple.mail/Data/Library/Mail Downloads
      请不要在终端用open命令,因为Mail Downloads这个有空格。
  • 备忘录、提醒事项
  • 钥匙串
  • 清除系统钥匙串中的密码等相关
  • 浏览器

    • Safari、Chrome账号&密码

    附:Mac如何查看Safari保存的密码

    • Safari、Chrome浏览记录
    • Safari、Chrome个人收藏
  • 微信记录
    在输入框中输入 ~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/,然后找到com.tencent.xinWeChat删除整个文件夹即可。

  • QQ记录
    在输入框输入~/Library/Containers/com.tencent.qq,删除整个文件即可。

  • Keynote、Page等清除打开的文件记录

    • 打开软件 –> 文件 –> 打开最近使用 –> 清除菜单
  • 应用软件账号删除(百度网盘、Evernote、SourceTree等)

  • 其他

    • iPhone模拟器重置

      1
      顶部 -> Erase All Content And Setting
  • 应用软件删除

  • 应用软件账号删除(百度网盘、Evernote、SourceTree等)

  • 文件夹删除(文稿、图片、下载内容等)

内存

内存

[toc]

必看文章:

什么是内存管理?是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

内存管理管理的是堆上的内存,栈上的内存并不需要我们管理。

说说内存管理

我们知道,当不再使用一个对象时应该将其释放,但是在某些情况下,我们很难理清一个对象什么时候不再使用(比如self.name不止在一个方法里会被调用到的时候),这可怎么办?ObjC提供autorelease方法来解决这个问题。

说到内存管理,我们就不得不提引用计数。当对象的引用计数为0,对象的内存就会被释放。

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。

当我们在创建一个对象的实例并在堆上申请内存时,或者在其他对象中需要持有这个对象时,该对象的引用计数就会加1;
当代码结束使用该对象/释放对象时,则将对象的引用计数减1;而想要释放一个对象的内存,我们将该对象的引用计数减到0,即进行release到0。
在ARC的情况下,为了方便管理内存,它会有一个autorelease的东西,它省去了我们还要自己去执行release方法的操作。同时与autorelease相关的还有一个叫AutoreleasePool自动释放池。

通过autorelease,当给一个对象发送autorelease消息(类方法创建的对象系统会自动添加autorelease)时,对象在接收到autorelease消息后,它会被添加到了当前的自动释放池autoreleasepool中。在未来某个时间,当自动释放池被销毁时,会给池里所有的对象发送release消息将其释放(释放≠销毁)。如果自动释放池向对象发送release消息后对象的引用计数变为了0,则改对象就会被销毁,内存就会被回收。在释放前这个时间段内,对象还是可以使用的(注意1:autorelease不会改变对象的引用计数,release才改变引用计数)(注意2:自动释放池实质上只是在释放的时候给池中所有对象对象发送release消息,不保证对象一定会销毁,如果自动释放池向对象发送release消息后对象的引用计数仍大于1,对象就无法销毁。

1
2
3
id array = [NSMutableArray arrayWithCapacity:1];
此源代码等同于以下源码。
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

所以AutoreleasePool的释放有如下两种情况。

  • 一是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。(一定要回答到runloop)。那runloop什么时候结束呢?

    一个RunLoop包含若干个Mode,每个Mode包含若干个Source/Timer/Observer/Port。当启动一个RunLoop时会先指定一个Mode,检查指定Mode是否存在以及Mode中是否含有SourceTimer,如果Mode不存在或者Mode中无SourceTimer,认为该Mode是一个空的ModeRunLoop就直接退出。

  • 二是手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool

AutoreleasePool(自动释放池)

1、AutoreleasePool(自动释放池)介绍

AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。即当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop结束或者作用域超出autoreleasepool{}之后再被释放。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。

我们把main.m文件通过Xcode自带的xcrun命令,来编译成main.cpp文件

命令行如下

1
xcrun -sdk iphonesimulator clang -rewrite-objc ./main.m

可以在编译出来的cpp文件中,看到如下自动释放池的结构体如下。

1
2
3
4
5
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} // 构造函数
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} // 析构函数
void * atautoreleasepoolobj;
};

objc4是我们通常说的Runtime源码,我们遇到的libobjc.A.dylib就是用它编译出来的。

源码查看,进入http://opensource.apple.com/source/搜索objc4,

image-20211103173107899

NSThread、NSRunLoop 和 NSAutoreleasePool三者之间的关系

  • NSThread 和 NSRunLoop是一一对应的关系
  • 在NSRunLoop对象的每个运行循环(event loop)开始前,系统会自动创建一个autoreleasepool,并在运行循环(event loop)结束时drain掉这个pool,同时释放所有autorelease对象
  • autoreleasepool只会对应一个线程,每个线程可能会对应多个autoreleasepool,比如autoreleasepool嵌套的情况
  • autorelease本质上就是延迟调用release方法
  • MRC环境,通过调用[obj autorelease]延迟内存的释放
  • ARC环境,甚至可以完全不知道autorelease也能管理好内存

看到这里有人可能会问,那到底延迟到什么时候执行呢?看完本文后,各位心中自然会有答案。

(3)、autorelease、autoreleasepool(自动释放池)
(4)、autoreleasepool(自动释放池)  

  这里说到的自动释放池,顾名思义,就是一个池,这个池可以容纳对象,而且可以自动释放,这就大大增加了我们处理对象的灵活性。   

(5)、autoreleasepool里面对象的内存什么时候释放?

  在runloop sleep的时候当前autoreleasePool drain(objc_autoreleasePoolPop) 掉,向里面的对象都发送release消息,建立一个新的autoreleasePool(objc_autoreleasePoolPush)。或者简单的说就是当@autoreleasepool结束时,里面的内存就会回收;

ARC时代,系统自动管理自己的autoreleasepool,runloop就是iOS中的消息循环机制,当一个runloop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次runloop迭代结束时清理掉被本次迭代期间被放到autorelease pool中的对象的。至于何时runloop结束并没有固定的duration。

(6)、runloop、autorelease pool以及线程之间的关系

每个线程(包含主线程)都有一个Runloop。对于每一个Runloop,系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个像callstack一样的一个栈式结构,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release。

##### (7)、自动释放池怎样创建

ObjC提供两种方法创建自动释放池:

方法一:使用NSAutoreleasePool来创建

1
2
3
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];
//这里写代码
[pool release];

方法二:使用@autoreleasepool创建

1
2
3
@autoreleasepool {
//这里写代码
}

自动释放池创建后,就会成为活动的池子,释放池子后,池子将释放其所包含的所有对象。

以上两种方法推荐第一种,因为将内存交给ObjC管理更高效。

(8)、自动释放池使用注意

1)自动释放池实质上只是在释放的时候给池中所有对象对象发送release消息,不保证对象一定会销毁,如果自动释放池向对象发送release消息后对象的引用计数仍大于1,对象就无法销毁。

2)自动释放池中的对象会集中同一时间释放,如果操作需要生成的对象较多占用内存空间大,可以使用多个释放池来进行优化。比如在一个循环中需要创建大量的临时变量,可以创建内部的池子来降低内存占用峰值。

3)autorelease不会改变对象的引用计数

(9)、自动释放池的应用/什么时候要用@autoreleasepool

有些情况下,我们还是需要手动创建自动释放池,那么,什么时候呢?

苹果文档中的翻译如下:

  1. 如果你正在编写不基于UI 框架的程序,比如命令行工具。
  2. 如果你编写的循环创建了很多临时对象。那么你可以在循环中使用自动释放池block,在下次迭代前处理这些对象。在循环中使用自动释放池block,有助于减少应用程序的内存占用。
  3. 你生成了一个辅助线程。 一旦线程开始执行你必须自己创建自动释放池。否则,应用将泄漏对象。

按我的理解,最重要的使用场景,应该是有大量中间临时变量产生时,避免内存使用峰值过高,及时释放内存的场景。

如在一个循环事件中,如果循环次数较大或者事件处理占用内存较大,就会导致内存占用不断增长,可能会导致不希望看到的后果。

举个例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding
error:&error];
}
}

//或
for (int i = 0; i < 100000; i ++) {
@autoreleasepool {
NSString * log = [NSString stringWithFormat:@"%d", i];
NSLog(@"%@", log);
}
}

如果这个for循环里不使用@autoreleasepool,虽然每个循环中生成的字符串对象都会放在自动释放池子中(假设是1号自动释放池),但是这个1号自动释放池是需要等到循环事件结束时释放的。这时候由于循环太大,势必会造成在循环期间内存不增长。所以,这里我们需要使用@autoreleasepool,额外创建一个2号自动释放池,来使得在每个@autoreleasepool结束时,里面的临时变量都会回收,内存使用更加合理。

例子2:假如有2000张图片,每张1M左右,现在需要获取所有图片的尺寸,你会怎么做?
  如果这样做

1
2
3
4
for (int i = 0; i < 2000; i ++) {
CGSize size = [UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg",i]].size;
//add size to array
}

  用imageNamed方法加载图片占用Cache的内存,autoReleasePool也不能释放,对此问题需要另外的解决方法,当然保险的当然是双管齐下了

1
2
3
4
5
6
   for (int i = 0; i < 2000; i ++) {
@autoreleasepool {
CGSize size = [UIImage imageWithContentsOfFile:filePath].size;
//add siez to array
}
}

常见笔试/面试题

< 返回目录

自动释放池底层怎么实现?

答:自动释放池以栈的形式实现:当你创建一个新的自动释放池时,它将被添加到栈顶.当一个对象收到发送autorelease消息时,他被添加到当前线程的处于栈顶的自动释放池中,当自动释放池被回收时,他们从栈中被删除,并且会给池子里面所有的对象都会做一次release操作

①语言Swift

①语言Swift

[Toc]

一、Copy

  1. 如何让自己的类用copy修饰符

    若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyingNSMutableCopying协议。
    具体步骤:
    1.需声明该类遵从NSCopying协议
    2.实现NSCopying协议的方法,具体区别戳这里

  • NSCopying协议方法为:
1
2
3
4
5
- (id)copyWithZone:(NSZone *)zone {
MyObject *copy = [[[self class] allocWithZone: zone] init];
copy.username = self.username;
return copy;
}

1、浅拷贝和深拷贝的区别

浅层复制:只复制指向对象的指针,而不复制引用对象本身。
深层复制:复制引用对象本身。

意思就是说我有个A对象,复制一份后得到A_copy对象后,

对于浅复制来说,A和A_copy指向的是同一个内存资源,复制的只不过是是一个指针,对象本身资源还是只有一份,那如果我们对A_copy执行了修改操作,那么发现A引用的对象同样被修改,这其实违背了我们复制拷贝的一个思想。
而对于深复制就好理解了,内存中存在了两份独立对象本身。

通俗的话将就是:
浅复制好比你和你的影子,你完蛋,你的影子也完蛋;
深复制好比你和你的克隆人,你完蛋,你的克隆人还活着。

2、 iOS - Copy 与 MutableCopy

iOS - Copy 与 MutableCopy
①copy:因为copy默认返回的是不可变的,所以当我们对一个不可变的字符串进行copy的时候,我们只是拷贝了它的指针(浅拷贝)。当我们对一个可变的字符串进行拷贝的时候,因为类型转变了,我们需对其进行深拷贝。
mutableCopy:默认返回的是一个可变的对象,适用于可变的对象,例如NSMutableString,NSMutableArray,NSMutableDictionary、etc。 无论对于可变的字符串还是不可变的字符串进行mutableCopy,系统都默认进行深拷贝。

其他参考

(1)、copy到底是深拷贝还是浅拷贝?

当我们对一个不可变对象(NSString类型)使用copy关键字的时候,系统是不会产生一个新对象,因为原来的对象是不能修改的,拷贝出来的对象也是不能修改的,那么既然两个都不可以修改,所以这两个对象永远也不会影响到另一个对象(符合我们说的“修改新(旧)对象,不影响旧(新)对象”原则),系统为了节省内存,所以就不会产生一个新的对象了。那么问题来了,copy到底是深拷贝还是浅拷贝?答:是否是深浅拷贝,是否创建新的对象,是由程序运行的环境所造成的,并不是一概而论。

(2)、这个写法会出什么问题@property (nonatomic, copy) NSMutableArray *mutableArray;

添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。原因:

self.mutableArray = xxx;在copy的修饰下执行的是 self.mutableArray = [xxx copy];进行了浅拷贝,得到的是一个xxx的副本,且该副本是一个不可变的数组。导致在运行的时候,其实你的mutableArray已经是NSArray类了。从而在添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。

3、为什么NSArray用copy修饰、NSMutableArray用strong修饰

把NSMutableArray用copy修饰有时就会crash,因为copy后的数组变成了不可变数组NSArray.当你对不可变数组NSArray进行增删改操作的时候就会crash,
举例如下:

1
2
3
4
5
6
7
①NSMutableArray用copy属性造成的crash:
@property (nonatomic, copy) NSMutableArray *mutableArray1; // 会崩溃
@property (nonatomic, strong) NSMutableArray *mutableArray2; // 正确

NSMutableArray *array = [NSMutableArray arrayWithArray:@[[Model1 new], [Model1 new]]];
self.mutableArray1 = array; // copy: mutableArray1是array的副本,且通过打印验证是浅拷贝。且此时aArray的值mArray的一个副本,该副本是通过[mArray copy]进行的浅拷贝得到的一个不可变新对象,即类型在执行时候变为了 NSArray了。所以,如果对归属为NSArray了的aArray执行NSMutableArray才有的如removeAllObjects的方法时,就会崩溃。
self.mutableArray2 = array; // strong:mutableArray1是array自身

当修饰可变类型的属性时,如NSMutableArray、NSMutableDictionary、NSMutableString,用strong。

当修饰不可变类型的属性时,如NSArray、NSDictionary、NSString,用copy。

4、模型数组深拷贝

通常需要实现对模型的拷贝都需要先实现NSCopying、 NSMutableCopying协议。注意:如果是数组使用拷贝操作是不会对数组内实现copy协议的对象进行深拷贝的。

参考文章:iOS 模型数组深拷贝

1、最笨的方法就是通过遍历逐个拷贝元素

1
2
3
4
NSMutableArray *array = [NSMutableArray array];
for (Person *person in dataSourceAry) {
[array addObject:[person copy]];
}

2、也有人使用归档解档实现数组内部元素拷贝

3、这么好用的一个方法现在才发现(推荐)

1
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag
1
2
3
4
5
6
7
8
NSArray <Person *>*deepCopyAry = [[NSArray alloc]initWithArray:dataSourceAry copyItems:YES];
NSLog(@"<dataSourceAry: %@>", dataSourceAry);
NSLog(@"<deepCopyAry: %@>", deepCopyAry);

[deepCopyAry enumerateObjectsUsingBlock:^(Person *obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.name = @"弗兰克";
obj.dog.name = @"弗兰克的dog";
}];

4、分析NSString、NSMutableString等类的copy、mutableCopy

在语言文章中,我们已经说明对于语句NSString *obj = [[NSData alloc] init]; obj在编译时是NSString的类型;运行时是NSData类型的对象。

由此得出的结论是,不要被编译时的类型蒙蔽,还要看实际运行时的类型。

(1)、分别对NSString、NSMutableString进行copy、mutableCopy生成的类型是什么?

你可以片面理解为

copy是[NSString alloc],所以生成的都是不可变的;
mutableCopy是[NSMutableString alloc],所以生成的都是可变的;

所以以下代码的结果,即为代码中的注释一样

1
2
3
4
5
6
7
8
9
10
>NSString *str1 = @"test001";
>
>NSMutableString *str2 = [str1 copy];
>//编译时,str2是NSMutableString类型。因为是把str2声明为可变字符串,所以str2即为声明的可变字符串
>//运行时,str2是NSString类型。因为是copy,所以不管str1是可变不可变,str2都是不可变字符串
>
>NSMutableString *str3 = [str1 mutableCopy];
>//编译时,str3是NSMutableString类型。因为是把str3声明为可变字符串,所以str3即为声明的可变字符串
>//运行时,str3是NSMutableString类型。因为是mutableCopy,所以不管str1是可变不可变,str3都是可变字符串
>
(2)、分别对NSString、NSMutableString进行copy、mutableCopy操作,是否会开辟新地址,即是属于深拷贝还是浅拷贝?

copy:对[string copy]是浅拷贝,即不会开辟新地址,而对[mutableString copy]是深拷贝,会开辟新地址。

mutableCopy:不管是对[string mutableCopy],还是对[mutableString mutableCopy],都是深拷贝,都会开辟新地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)testCopy {
NSString *testStr = [NSString stringWithFormat:@"123"];
id copyedStr = [testStr copy];
id mutableCopyStr = [testStr mutableCopy];
NSLog(@"=== 对NSString变量进行copy、mutableCopy得到的地址和类型分别是 ===");
NSLog(@"testStr = %p, class: %@", testStr, NSStringFromClass([testStr class]));
NSLog(@"copyedStr = %p, class: %@", copyedStr, NSStringFromClass([copyedStr class]));
NSLog(@"mutableCopyStr = %p, class: %@", mutableCopyStr, NSStringFromClass([mutableCopyStr class]));

NSLog(@"\n");

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
id copyedMutableStr = [testMutableStr copy];
id mutableCopyMutableStr = [testMutableStr mutableCopy];
NSLog(@"=== 对NSMutableString变量进行copy、mutableCopy得到的地址和类型分别是 ===");
NSLog(@"testMutableStr = %p, class: %@", testMutableStr, NSStringFromClass([testMutableStr class]));
NSLog(@"copyedMutableStr = %p, class: %@", copyedMutableStr, NSStringFromClass([copyedMutableStr class]));
NSLog(@"mutableCopyMutableStr = %p, class: %@", mutableCopyMutableStr, NSStringFromClass([mutableCopyMutableStr class]));
}

对NSString、NSMutableString变量进行copy、mutableCopy得到的地址和类型分别是

结论:分别对NSString、NSMutableString进行copy、mutableCopy操作,只有NSString copy是浅拷贝

(3)、将NSString、NSMutableString变量赋值给用copy、strong修饰NSString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?(附:NSString用copy修饰是为什么)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@property (nonatomic, copy) NSString *copyedStr;
@property (nonatomic, strong) NSString *strongStr;

@property (nonatomic, copy) NSString *copyedMutableStr;
@property (nonatomic, strong) NSString *strongMutableStr;

///将NSString、NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候
- (void)testCopy {
NSString *testStr = [NSString stringWithFormat:@"123"];
self.copyedStr = testStr;
self.strongStr = testStr;
NSLog(@"=== 将NSString变量赋值给用copy、strong修饰的NSString属性的时候 ===");
NSLog(@"testStr = %p", testStr);
NSLog(@"copyedStr = %p", self.copyedStr);
NSLog(@"strongStr = %p", self.strongStr);

NSLog(@"\n");

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.copyedMutableStr = testMutableStr;
self.strongMutableStr = testMutableStr;
NSLog(@"=== 将NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候 ===");
NSLog(@"testMutableStr = %p", testMutableStr);
NSLog(@"copyedMutableStr = %p", self.copyedMutableStr);
NSLog(@"strongMutableStr = %p", self.strongMutableStr);
}

将NSString、NSMutableString赋值给用copy、strong修饰的NSString变量

可以看出,

①将NSString变量赋值给用copy、strong修饰的NSString属性的时候,不管是strong还是copy属性的对象,其指向的地址都是同一个,即为string指向的地址。

1
2
3
4
5
6
@property (nonatomic, copy) NSString *copyedStr;
@property (nonatomic, strong) NSString *strongStr;

NSString *testStr = [NSString stringWithFormat:@"123"];
self.copyedStr = testStr;
self.strongStr = testStr;

②将NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候,

此时copy属性字符串copyedMutableStr

1
2
3
4
@property (nonatomic, copy) NSString *copyedMutableStr;

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.copyedMutableStr = testMutableStr;

深拷贝了testMutableStr字符串,并让copyedMutableStr对象指向这个字符串(即copyedMutableStr和testMutableStr只是对象值一样,但不是同一个了)。所以此时,我们如果去修改testMutableStr字符串的话,可以看到,我们用@property (nonatomic, copy) NSString *copyedMutableStr;修饰的copyedMutableStr能够不会因为其赋值源testMutableStr的改变而改变,也就保证了安全性。

而strong属性字符串strongMutableStr

1
2
3
4
@property (nonatomic, strong) NSString *strongMutableStr;

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.strongMutableStr = testMutableStr;

因为strongMutableStr与testMutableStr是指向同一对象,所以strongMutableStr的值也会跟随着改变;

综上:所以,在声明NSString属性时,到底是选择strong还是copy,可以根据实际情况来定。不过,一般我们将对象声明为NSString时,都不希望它改变(包括不希望赋值后,其他原来的值的改变会改变到它),所以大多数情况下,我们建议用copy,以免因可变字符串的修改导致的一些非预期问题。使用copy来修饰无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本,这样更安全。

(4)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSMutableString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
1
2
3
4
5
6
7
8
9
10
11
12
13
@property (nonatomic, copy) NSMutableString *copyedStr;
@property (nonatomic, strong) NSMutableString *strongStr;

NSString *testStr = [NSString stringWithFormat:@"123"];
self.copyedStr = testStr;
self.strongStr = testStr;



@property (nonatomic, copy) NSMutableString *copyedMutableStr;

NSMutableString *testMutableStr = [NSMutableString stringWithFormat:@"mutable_456"];
self.copyedMutableStr = testMutableStr;

2.自定义类如何让它具有copy功能?
遵守NScoping协议,实现copywithzone方法即可.

(5)、自己代码实现copy修饰符,应该怎么写????

4、NSCoding和NSCopy

NSCoding和NSCopy

(1)、NSCoding的作用

iOS通过NSCoding保存实体对象

很多时候我们都需要将对象序列化,比如将一个对象存入到NSUserDefault 里面去的时候,由于NSUserDefault支持存入的类型有限制,所以很多时候我们需要将NSObject类型的对象转换成NSData再存入进去。

(2)、NSCopy

当你要进行对象拷贝的时候需要遵循NSCopy协议

1
2
3
4
5
6
7
8
9
- (id)copyWithZone:(NSZone *)zone {
id copy = [[[self class] alloc] init];
if (copy) {
[copy setId:[self.id copyWithZone:zone]];
[copy setNickName:[self.nickName copyWithZone:zone]];
}

return copy;
}

5、@synthesize和@dynamic区别

@synthesize和@dynamic区别

在声明property属性后,有2种实现选择

  • @synthesize

编译器期间,让编译器自动生成getter/setter方法。当有自定义的存或取方法时,自定义会屏蔽自动生成该方法

  • @dynamic

告诉编译器,不自动生成getter/setter方法,避免编译期间产生警告,然后由自己实现存取方法,或存取方法在运行时动态创建绑定:主要使用在CoreData的实现NSManagedObject子类时使用,由Core Data框架在程序运行的时动态生成子类属性

内存-①基础

①基础

[toc]

知识架构

iOS知识库

Android知识库

目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1、几个本质
>
2、空指针和未初始化的指针
(1)、空指针和未初始化的指针的区别:
(2)、为什么指针变量定义时一定要初始化?
>
3、野指针与悬空指针
(1)、指针与内存的常见使用顺序
(2)、内存泄漏的概念
(3)、野指针概念
(4)、在iOS中野指针的后果
(5)、分析野指针的产生原因及解决办法
>
4、iOS NSerror 用双重指针理解
>
5、指针和引用的区别
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1、堆和栈的区别?
>
2、浅拷贝和深拷贝的区别
>
3、分析NSString、NSMutableString等类的copy、mutableCopy
(1)、分别对NSString、NSMutableString进行copy、mutableCopy生成的类型是什么?
(2)、分别对NSString、NSMutableString进行copy、mutableCopy操作,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
(3)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
(4)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSMutableString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
(5)、自己代码实现copy修饰符,应该怎么写????
>
4、NSCoding和NSCopy
(1)、NSCoding的作用
(2)、NSCopy
>
5、@synthesize和@dynamic区别
>
1
2
3
4
5
6
7
8
9
10
11
12
13
1、内存管理
(1)、在ObjC中,对象什么时候会被释放(或者对象占用的内存什么时候会被回收利用)?
(2)、那怎么知道对象已经没有被引用了呢?
(3)、autorelease、autoreleasepool(自动释放池)
(4)、autoreleasepool(自动释放池) 
(5)、autoreleasepool里面对象的内存什么时候释放?
(6)、runloop、autorelease pool以及线程之间的关系
(7)、自动释放池怎样创建
(8)、自动释放池使用注意
(9)、自动释放池的应用/什么时候要用@autoreleasepool
>
2、如何监测内存泄漏
>
1
2
自动释放池底层怎么实现?
>

常用数据类型占用内存大小

64位编译器

char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 8个字节
long long: 8个字节
unsigned long: 8个字节

一、指针

< 返回目录

1、几个本质

1
2
3
4
5
6
7
8
9
10
11
12
13
数据类型:
数据类型的本质是固定大小内存的别名。
对变量声明数据类型,是为了告诉编译器分配几个字节的内存。

变量:
变量的本质是一段内存空间的别名。
也就是给一段内存空间取一个新的名字,就是变量。

指针:
指针也是一种数据类型,它的值是某一个内存空间的地址。
指针的步长根据它指向的内存空间的数据类型而定。

数组中[]的本质:假如有数组array,则array[i]等价于*(array+i),是因为[]对于程序员来讲是友好的,但是编译器最终还是要将它理解为指针,也就是数组作为函数参数时的退化。array[i] ==> array[0+i] ==>*(array+i)

2、空指针和未初始化的指针/野指针

(1)、空指针和未初始化的指针的区别:

①空指针可以确保不指向任何对象或函数;
②未初始化指针则可能指向任何地方,即它所指向的地址就是随机的,也就说此时它是个野指针。(附:如果一个指针的指向对象后来被删除,却未置为空指针nil,则它也是野指针)
所以空指针在概念上不同于未初始化的指针。
对于malloc在其内存分配的时候,如果内存分配成功,返回的一定不是空指针;但是如果malloc内存分配失败,返回的空指针。而不是一个未初始化的指针。

以下是华为笔试题:

1
2
3
4
5
下面有关空指针和未初始化指针,说法错误的是?
A.对0x0这个地址取值是非法的
B.空指针可以确保不指向任何对象或函数; 而未初始化指针则可能指向任何地方。
C.空指针与任何对象或函数的指针值都不相等
D.malloc在其内存分配失败时返回的是一个未初始化的指针

错误答案是D,因malloc内存分配失败,返回的是空指针。详细请查看原文地址
华为笔试:下面有关空指针和未初始化指针,说法错误的是?

(2)、为什么指针变量定义时一定要初始化?

答:因为你首先要理解一点.内存空间不是你分配了才可以使用,只是你分配了之后使用才安全。

为什么要进行对他初始化呢,因为如果你没对它初始化,那么这个指针所指向的地址就是随机的,即此时它是个野指针。这时候如果你引用这个指针并对它做了修改这个指针所指向的内容的操作的话,如果刚好这个指针所指向的内容恰好是另外一个程序的数据的话,那么你原本随意的一个修改,就造成了对另一个程序的数据的修改了,也就会导致另外一个程序可能不能正常运行了。所以使用前一定要进行初始化。

3、野指针与悬空指针

在C/C++等语言中,

悬空指针(Dangling Pointer)指的是:一个指针的指向对象已被删除,那么就成了悬空指针。

野指针是那些未初始化的指针

有时也把野指针和悬空指针通称悬空指针。
而好像在iOS中是通称为野指针。

以下内容摘自:百度百科:迷途指针

在计算机编程领域中,迷途指针,或称悬空指针、野指针,指的是不指向任何合法的对象的指针。

当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称迷途指针。

若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,则将产生无法预料的后果。因为此时迷途指针所指向的内存现在包含的已经完全是不同的数据。通常来说,若原来的程序继续往迷途指针所指向的内存地址写入数据,这些和原来程序不相关的数据将被损坏,进而导致不可预料的程序错误。

这种类型的程序错误,不容易找到问题的原因,通常会导致存储器区块错误(Linux系统中)和一般保护错误(Windows系统中)。如果操作系统的内存分配器将已经被覆盖的数据区域再分配,就可能会影响系统的稳定性。

某些编程语言允许未初始化的指针的存在,而这类指针即为野指针。野指针所导致的错误和迷途指针非常相似,但野指针的问题更容易被发现。

(1)、指针与内存的常见使用顺序

在堆中申请了一块内存,并用一个指针指向它。
一般我们都会在不用的时候先释放该指针指向的内存,再将该指针置为空指针。
即一般正确的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//指针变量和指针所指向的内存变量是两个不同的概念
//使用动态内存分为三步
//1.定义时,将指针为定义NULL
//2.释放内存时,把指针变量重新赋值或者NULL
//3.释放内存后,把指针变量赋值为NULL

#include <stdio.h>

int main02()
{
int *p = NULL;
p = (int *)malloc(4);

if (p != NULL){
free(p);//释放P所指向的内存空间,但指针变量p仍然留在栈中,成为了野指针
p = NULL;//释放野指针(这是关键,记得释放指针所指向的内存空间后,要释放野指针)
}

return 0;
}

但是
①、如果我们在未来始终没有去手动释放掉我们开辟的内存的话,会导致内存泄漏;
②、如果释放掉了该内存,却忘了同时释放只想该内存的指针,会导致产生悬空指针或者说是迷途指针,或者有人也称是野指针。
③、如果记得释放指针,却忘了释放指针只想的内存(即记得②忘了①),那么由于指针已经消失,而指针指向的东西还在,那么久永远无法控制这块内存,而导致一定内存泄漏了。

(2)、内存泄漏的概念

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

(3)、野指针概念

C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指针 指向1块随机的内存空间。

OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.

野指针:指向内存被释放的内存或者没有访问权限的内存的指针

更详细的概念可查看:百度百科——野指针

(4)、在iOS中野指针的后果

野指针的后果:崩溃EXC_BAD_ADDRESS

(5)、分析野指针的产生原因及解决办法

知识点:任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

“野指针”的成因主要有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1)指针变量没有被初始化。(即随机只想的这个指针很有可能只想一块没人用的内存,)
char *p; //此时p为野指针
//正确的为在声明之后加上 p=new char[10];我们常直接写为一行,即char *p=new char[10];

2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针.
char *p=new char[10]; //指向堆中分配的内存首地址,p存储在栈区
cin>> p;
delete []p; //p重新变为野指针
//正确的应该在指针p被free或者delete之后,加上p = Null;

3)指针操作超越了变量的作用范围。
char *p=new char[10]; //指向堆中分配的内存首地址
cin>> p;
cout<<*(p+10); //可能输出未知数据

iOS 关于僵尸对象和僵尸指针的那些事儿

4、iOS NSerror 用双重指针理解

常见代码如下:

1
2
3
4
5
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtPath:absoluteFilePath error:&error];
if (error) {
NSLog(@"删除单个文件的时候出现错误:%@",error.localizedDescription);
}

可见,如果我们不是传指针的指针&error,而是传error,那么if(error)中的error就肯定是nil了。那就没用了。

所以这里传error的指针的原因可简单概括为:
因为我们要得到一个新的error值。所以如果有方法

1
2
3
4
5
- (NSError *)getNewErrorForremoveItemAtPathremoveItemAtPath:(NSString *)path  error:(NSError **)error {
NSError *newError = [NSError errorWithDomain:...];

return newError;
}

那么这边,我们的error,就可以直接传error本身,甚至不传都是可以的。

但是实际是这些方法本身的返回值,已经被定义为判断能否进行某种操作,而不是操作是否成功给占用了,如这边已经被判断能否进行删除文件给占用了,所以如果我们还想知道这个删除文件操作结果的error,那就把error的指针的指针传进去,最后其出来的就是我们想要的。
所以,猜测其内部结构应该是

1
2
3
4
5
6
7
8
9
10
- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error {
BOOL canRemove = ...;
if (!canRemove) {
* error = [NSError errorWithDomain:...];
return NO;
} else {
//* error = * error; //即不变
return YES;
}
}

以下解释原因摘自:ios中处理错误为什么传递的是&error,而不是error?

因为 需要将error 传入后修改其值,然后再返回来,返回来后还要保证己经修改过了。

&error传入是传的地址引用,传入后处理函数直接访问变量的地址,可以修改其值再返回同一个地址, 调用函数就可以知道值是否有修改,即是否有错。

而error传入是传的值引用,值引用传入到程序栈中后其实是把原来的值复制了一份传过去,处理函数可以修改,但无法将改后的值传出函数体。

5、指针和引用的区别

(1)定义和性质的区别
①指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;
②而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
简单点说:一个是存地址,一个是变量别名
(2)指针和引用作为函数参数进行传递时的区别
①用指针传递参数,可以实现对实参进行改变的目的,是因为传递过来的是实参的地址
②引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

二、内存

< 返回目录

1、堆和栈的区别?

1
2
3
4
5
6
7
一、堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2、堆(操作系统):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

二、堆栈缓存方式区别:
1、栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

八、谈谈内存管理、内存泄露、循环引用

< 返回目录

ARC已经出来很久了,自动释放内存的确很方便,但是并非绝对安全绝对不会产生内存泄露。

1、内存管理

(1)、在ObjC中,对象什么时候会被释放(或者对象占用的内存什么时候会被回收利用)?

答案是:当对象没有被任何变量引用(也可以说是没有指针指向该对象)的时候,就会被释放。

(2)、那怎么知道对象已经没有被引用了呢?

ObjC采用引用计数(reference counting)的技术来进行管理:

1
2
3
4
1)每个对象都有一个关联的整数,称为引用计数器;
2)当代码需要使用该对象时,则将对象的引用计数加1;
3)当代码结束使用该对象时,则将对象的引用计数减1;
4)当引用计数的值变为0时,表示对象没有被任何代码使用,此时对象将被释放。

与之对应的消息发送方法如下:

对象操作 OC中对应的方法 引用计数的变化
当对象被创建时 alloc/new/copy/mutableCopy等 +1
持有对象 retain +1
释放对象 release -1
废弃对象 dealloc -
1
2
3
4
5
6
7
8
1)当对象被创建(通过alloc、new或copy/mutableCopy等方法)时,其引用计数初始值为1;
2)给对象发送retain消息,其引用计数加1;
3)给对象发送release消息,其引用计数减1;
4)当对象引用计数归0时,ObjC給对象发送dealloc消息销毁对象
当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1;
在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1;
需要释放一个对象时,就将该对象的引用计数减1;
直至对象的引用计数为0,对象的内存会被立刻释放。
1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

NSObject *object = [[NSObject alloc] init]; // 执行后,引用计数 = 1
NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x", (unsigned long)[object retainCount], object, &object);
self.property = object; // 执行后,引用计数 = 2
NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x \n property指针内存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
[object release]; // 执行后,引用计数 = 1
NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x \n property指针内存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
return YES;
}

当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

__weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。

2、如何监测内存泄漏

如果内存管理不当,势必会造成内存泄露。那我们如何快速的来找出内存泄露呢?以前我们可能会使用Instruments来监测,但是我们会发现使用Instruments特别繁琐,而且不一定能定位到内存泄露。

所以这里伟大的Facebook工程师们开源了一些自动化工具来解决监测内存泄露问题:FBRetainCycleDetector、FBAllocationTracker、FBMemoryProfiler。详情查看在iOS上自动检测内存泄露

你在开发大型项目时,如何进行内存泄露检测的?

instruments下有个leaks工具,启动此工具后,运行项目,工具里可以显示内存泄露的情况,双击可找到源码位置,可以帮助进行内存泄露的处理。

END

< 返回目录

1
2


内存-②Block

内存-②Block

[toc]

iOS-Block本质

iOS-Block本质

block本质上也是一个OC对象,它内部也有个isa指针,最终继承NSObject。

1
NSGlobalBlock <--__NSGlobalBlock <--NSBlock <-- NSObject

Q:block有哪几种类型 及 各类型的block在内存中如何分配的?

block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。

:动态分配内存,需要程序员自己申请,程序员自己管理

:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况

  • NSGlobalBlock 在数据区

    1
    2
    3
    void (^block1)(void) = ^{
    NSLog(@"block1");
    };
  • NSMallocBlock 在堆区

    1
    2
    3
    4
    int age1 = 1;
    void (^block2)(void) = ^{
    NSLog(@"block2:%d", age1);
    };
  • NSStackBlock 在栈区

    1
    2
    3
    4
    int age2 = 2;
    NSLog(@"%@", [^{
    NSLog(@"block3:%d", age2);
    } class]);

附:打印block类型NSLog(@"%@", [block1 class]);

问:以下各情况,Person的对象什么时候才销毁?或Person对象在block上是如何操作的?

有Person类如下:

1
2
3
4
5
6
7
8
9
#import "Person.h"

@implementation Person

- (void)dealloc {
NSLog(@"Person dealloc");
}

@end

了解解题的基础知识:

1、当对象类型的auto变量被block内部访问时,是强引用还是弱引用?(延伸:引用过后,block内部无法修改auto变量值,除非使用__block)

  • 如果block在空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象
  • 如果block在空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

2、GCD API的方法参数block,在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上。

附:在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况?

  • 1.block作为函数返回值时

  • 2.将block赋值给__strong指针时

  • 3.block作为Cocoa API中方法名含有usingBlock的方法参数时

  • 4.block作为GCD API的方法参数时

    1
    2
    3
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    });

1、问: 下列代码中的Person的对象什么时候才销毁?

1
2
3
4
5
6
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
Person *person = [[Person alloc] init];
person.age = 10;

NSLog(@"age:%d", person.age);
}

答:方法代码执行完就person就销毁了

2、问:以下gcd的block中引用 Person的对象什么时候才销毁?

1
2
3
4
5
6
7
8
9
10
11
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
Person *person = [[Person alloc] init];
person.age = 10;

// 不使用__weak修饰
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"age:%d", person.age); // 堆block会对Person强引用,故而也就只有block销毁时候Person才会被释放。这里是2秒后销毁。
});

NSLog(@"touchesBegan");
}

答:

1、gcd的block默认会做copy操作,即dispatch_after的block是堆block。

2、我们知道如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

所以这里外部的Person没有声明为__weak,所以堆block会对Person强引用,故而也就只有block销毁时候Person才会被释放。这里是2秒后销毁。(如果没有gcd)

3、续问:如果上述Person被添加上__weak修饰,那Person什么时候释放?会造成什么问题?

续答:此时上述的代码会变为

1
2
3
4
5
6
7
8
9
10
11
12
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
Person *person = [[Person alloc] init];
person.age = 10;

// 不使用__weak修饰
__weak Person *weakPerson = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"person:%p", weakPerson); // 当Person被添加上__weak修饰后,堆block会对Person弱引用。而在touchesBegan函数结束后,由于Person就会被释放,所以2秒后gcd无法捕捉到Person的后果。所以此处person 是 nil,打印的%p地址,是0x0。
});

NSLog(@"touchesBegan");
}

同样,还是我们知道如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用。所以当Person被添加上__weak修饰后,堆block会对Person弱引用。而在touchesBegan函数结束后,由于Person就会被释放,所以2秒后gcd无法捕捉到Person的后果。即上述的输出结果是

1
person 是 nil,打印的%p地址,是0x0。

4、上述代码使用__weak后,变量被释放了,那怎么防止block持有的对象提前释放

Q:block的属性修饰词为什么是copy?

block一旦没有进行copy操作,就不会在堆上
block在堆上,程序员就可以对block做内存管理等操作,可以控制block的生命周期

Q:当block被copy到堆时,对__block修饰的变量做了什么?

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain)
  • 对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用

Q:当block从堆中移除时,对__block修饰的变量做了什么?

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)

二、__block修饰符

Q:__block int age = 10,系统做了哪些?

Q:__block 修饰符作用?

内存-②循环引用Timer

内存-②循环引用Timer

目录

1
2
3
4
5
6
7
8
9
1、NSTimer和NSRunLoop的关系?
2、NSTimer使用细节
3、NSTimer的创建
4、NSTimer的循环引用
5、NSTimer使用的优化
>
6、NSTimer的销毁问题
(1)、子线程中NSTimer的创建和销毁问题
>

九、NSTimer

< 返回目录

1、NSTimer的创建

NSTimer的创建通常有两种方式,尽管都是类方法,一种是timerWithXXX,另一种scheduedTimerWithXXX。

二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是无法正常工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@interface ViewController1 ()
@property (nonatomic, weak) NSTimer *timer1;
@property (nonatomic, weak) NSTimer *timer2;
@end


@implementation ViewController1

- (void)viewDidLoad {
[super viewDidLoad];

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

/*
错误做法:
self.timer2 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval2:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer2 forMode:NSDefaultRunLoopMode];
*/

//正确做法:
//特别注意:timer2创建时并没直接赋值给timer2。
//原因是timer2是weak属性,如果直接赋值给timer2会被立即释放。
//因为timerWithXXX方法创建的NSTimer默认并没有加入RunLoop,只有后面加入RunLoop以后才可以将引用指向timer2。从而导致执行到addTimer:forMode的时候,访问了野指针而发生EXC_BAD_ACCESS,崩溃。
NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval2:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
。。。。。
}


- (void)timeInterval1:(id)object {
NSLog(@"111");
}

- (void)timeInterval2:(id)object {
NSLog(@"222");
}

@end

2、NSTimer的修饰符

runloop强制持有timer(runloop->timer),timer会强制持有其target,未处理的情况下一般都是self(timer->self),导致self无法释放。
虽然设置timer为weak属性时候,self未强制持有timer,没构成循环应用,但还是导致了self无法释放的问题,dealloc无法执行。

2、NSTimer的循环引用

关于循环引用,我们先看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController1 ()
@property (nonatomic, strong) NSTimer *timer;//注意这里的属性不是为weak,从而很容易引起循环引用
@end


@implementation ViewController1

- (void)viewDidLoad {
// 代码标记1 (产生timer与self之前的强引用,如下图中的L3强引用线)
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
// 代码标记2 (产生RunLoop与timer之间的强引用,如下图中的L4强引用线)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 代码标记3 (产生self与timer之间的强引用,,如下图中的L2强引用线)
self.timer = timer;
}

- (void)timerFire {
NSLog(@"timer fire");
}

@end

假设代码中的视图控制器由UINavigationController管理,由于self.timer是strong类型,则强引用可以表示如下:

NSTimer循环引用例子1

由于,很容易看出来,由于timer本身在创建时候已经与self发生了强引用(不管target使用weak还是strong修饰,timer都会对target强引用。)。而赋值时候又由于timer是被设为strong的,而导致self与timer之间也发生了强引用,最终这两个强引用,就形成了循环引用。

所以,

①、首先,我们先解决循环引用,为了解决timer的循环应用问题,我们上面的timer属性应该使用weak。设置成weak后,L2就消失了。

②、但是即使使用了弱引用,上面的代码中ViewController1在pop退出的时候也无法正常释放,原因是在创建NSTimer时指定了target为self,这样一来造成了timer对ViewController1有一个强引用。从而导致,timer没释放的时候,viewController也是不会被释放的。为了让timer能够释放,我们就需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。所以,假设在viewController pop回去前,我们通过一个按钮来让timer调用invalidate方法,那么viewController在pop回去时候,就能够被释放了。

问题是我们一般不会有这个按钮操作,那么这时候让timer调用invalidate方法的操作,应该写在哪里呢。这时候,你可能会想到那就写在viewDidDisappear中呗。但是一旦在viewWillDisappear中写[timer invalidate]; timer = nil;那么你也得把timer的创建放在viewWillAppear中。因为我们可能执行的是push跳到下一页,再返回来的操作。然而显然将timer的创建放在viewWillAppear中这样的方式,显然会是导致当timer可能需要频繁添加。所以,我们放弃此方法,还是把timer的创建放在viewDidLoad中,然后考虑其他方法。

所以,我们最后为了让ViewController1在pop退出的时候不会因为timer的强引用,而导致无法正常释放。我们选择转移timer中的target。这样就能确保,viewController在pop退出的时候能够正常释放,从而调用viewController的dealloc方法。

附:转移timer中的target的方法通常有两种:

一种是将target分离出来独立成一个对象(在这个对象中创建NSTimer并将对象本身作为NSTimer的target),控制器通过这个对象间接使用NSTimer;

另一种方式的思路仍然是转移target,只是可以直接增加NSTimer扩展(分类),让NSTimer自身做为target,同时可以将操作selector封装到block中。

NSTimer转移target方法二
图中参考NSTimer+Block

后者相对优雅,也是目前使用较多的方案。显然Apple也认识到了这个问题,如果你可以确保代码只在iOS 10下运行就可以使用iOS 10新增的系统级block方案(上面的代码中已经贴出这种方法)。

③、如果不做②中的转移timer的target的话,那么viewController就会无法释放,造成内存泄露。
但是,我们发现通过转移timer的target后,虽然解决了UIViewController1因为被timer强引用而导致的在pop回来的时候无法释放的问题。我们的计时器,却在UIViewController1 pop退出被释放后,两个定时器仍然在运行,也就是它还没被释放。所以,我们还需要解决timer的释放。那怎么让timer释放呢?答:如果要让timer释放掉的话,需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。
invalidate方法有2个功能:一是将timer从runloop中移除,那么图中的L4就消失,二是timer本身也会释放它持有资源,比如它的target、userinfo、block等,因为这里的target是self,所以强引用L3也就消失。
所以,最终当viewController在pop退出的时候,其正常释放并调用了dealloc放。我们只需要在dealloc方法中,添加上timer调用invalidate的方法,即可以解决viewController被释放了,但timer没被释放的问题。

1
2
3
4
- (void)dealloc {
[self.timer invalidate];
NSLog(@"ViewController1 dealloc...");
}

所以,一个完整的timer过程,代码如下:

1

3、NSTimer和NSRunLoop的关系?

只要出现NSTimer必须要有NSRunLoop,NSTimer必须依赖NSRunLoop才能执行 。NSTimer其实也是一种资源,如果看过多线程编程指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会生效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。

NSRunLoop与timer有关方法为:

1
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode; //在run loop上注册timer

注意事项:

我们通常在主线程中使用NSTimer,有个实际遇到的问题需要注意。当滑动界面时,系统为了更好地处理UI事件和滚动显示,主线程runloop会暂时停止处理一些其它事件,这时主线程中运行的NSTimer就会被暂停。解决办法就是改变NSTimer运行的mode(mode可以看成事件类型),不使用缺省的NSDefaultRunLoopMode,而是改用NSRunLoopCommonModes,这样主线程就会继续处理NSTimer事件了。具体代码如下:

1
2
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

4、NSTimer使用细节:

NSTimer上的定时任务是在创建NSTimer的线程上执行的。NSTimer的销毁和创建必须在同一个线程上操作
NSTimer要被添加到当前线程的 Runloop 里面且 Runloop 被启动,定时任务(selector或者invocation)才会触发。

以下内容摘自:NSTimer定时器进阶——详细介绍,循环引用分析与解决

  1. 它需要被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
  2. 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的
  3. runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
  4. timer的执行时间并不准确,系统繁忙的话,还会被跳过去。(具体的两种不准时,请查看原文)
  5. invalidate调用后,timer停止运行后,就一定能从runloop中消除吗,资源????invalidate方法的调用必须在timer添加到的runloop所在的线程,如果不在的话:由于调用invalidate 方法后,timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。

NSTimer的强引用问题举例:

NSTimer的强引用问题举例

5、NSTimer使用的优化

问:为什么要在非主线程创建NSTimer?

  • 将 timer 添加到主线程的Runloop里面本身会增加线程负荷;
  • 如果主线程因为某些原因阻塞卡顿了,timer 定时任务触发的时间精度肯定也会受到影响;
  • 有些定时任务不是UI相关的,本来就没必要在主线程执行,给主线程增加不必要的负担。当然也可以在定时任务执行时,手动将任务指派到非主线程上,但这也是有额外开销的。

6、NSTimer的销毁问题

前面我们已经简单讲过要让NSTimer销毁释放的时候,只能通过调用其invalidate来达到销毁目的。关于invalidate的第一个作用以及它在哪个线程调用的问题,我的理解如下:

invalidate方法的第一个作用将timer从runloop中移除。这里的runLoop指的应该是当前的runLoop,而不是timer被添加到的runLoop,这个纯属个人理解,未验证,因为如果是其添加的runLoop的话,那子线程timer的销毁,就不会有人说还要和timer所在的线程一致了。所以,这里的个人理解有如下:

为了销毁timer和去除runloop与timer之间的强引用,我们调用了timer的invalidate方法。

1、对于invalidate方法的调用是写在必须在timer所添加到的runloop所在的线程(如主线程)的时候,invalidate方法会将timer从runloop中移除,并且释放它持有资源。即上面的L4和L3都消失。

2、对于invalidate方法的调用不是写在必须在timer所添加到的runloop所在的线程的时候(如子线程中添加timer,在主线程中调用该timer的invalidate),虽然timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。

如果invalidate方法的调用的位置不更改的话,这时候要让L4消失的方法,

方法①手动销毁runloop。比如

1
2
3
4
    //[[NSRunLoop currentRunLoop] run]; //将原本的方法注释掉
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5.f];
[[NSRunLoop currentRunLoop] runUntilDate:date]; //让runloop在5s后销毁
>

这种方式,只适用于销毁时间确定的情况。那如果销毁时间不确定怎么办?

方法②:因为当某个线程销毁时,其runloop也随之销毁,所以方法二即为通过销毁timer所在的线程,来达到销毁runloop的目的。如果是在主线程,线程一直存在,我们没法让主线程销毁。

所以,下面我们讨论的是在子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?

6.1子线程中NSTimer的创建和销毁问题

我们按上诉2中②的讨论描述的:子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,写出的对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@property (nonatomic, weak) NSTimer *threadTimer; //子线程timer


- (void)viewDidLoad {
[super viewDidLoad];

// 开辟子线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
}

- (void)newThread {
@autoreleasepool {
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:@"这是子线程"];

self.threadTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(threadTimerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}

- (void)threadTimerAction {
static NSInteger counter = 0;

NSString *isMainThreadDescription = [NSThread isMainThread] ? @"YES" : @"NO";
NSLog(@"当前方法执行的线程:%@, 它是否是主线程:%@, counter = %@", [NSThread currentThread], isMainThreadDescription, @(counter++));
}

这时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?即怎么销毁线程?

乍看当在子线程开启runloop后,timer会一直在子线程中运行,所以子线程不会销毁,runloop也就无法停止,runloop也就没法销毁,runloop与timer之间的强引用则还是被保留着,这似乎又是个死循环。但实际上,由于上述代码runloop的mode item只有Timer,所以只要销毁timer,runloop就会退出。所以,上述的代码是没问题的,不存在内存泄露问题。

附:NSTimer上的定时任务是在创建NSTimer的线程上执行的。

附:以上NSTimer的内容,有空的话还可参考NSTimer,NSRunLoop,autoreleasepool,多线程的爱恨情仇,它那边讲的,和这边自己理解的基本是一样的。只是对于有些点的介绍详细不一定一样而已。

其他有空可看iOS 中的 NSTimer

常见笔试/面试题

< 返回目录

END

< 返回目录

内存-③内存泄漏定位

内存-③内存泄漏定位

未整合的文章:

目录

1
2
3
4
5
6
7
8
1、常见的三种泄露情形
(1)、创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to 'number' is never read。翻译一下:存储在'number'里的值从未被读取过。
(2)、创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to 'str' during its initialization is never read
(3)、调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into 'subImageRef'。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。
>
2、.....
>
>
1
2
3
4
5
6
7
8
1、ARC下dealloc的使用
ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。
>
2、controller 不能释放,不走dealloc方法的几种可能
>

以下内容摘自:iOS性能优化之内存管理:Analyze、Leaks、Allocations的使用和案例代码

前言

内存空间的划分: 我们知道,一个进程占用的内存空间,包含5种不同的数据区:

1
2
3
4
5
(1)BSS段:通常是存放未初始化的全局变量;
(2)数据段:通常是存放已初始化的全局变量。
(3)代码段:通常是存放程序执行代码。
(4)堆:通常是用于存放进程运行中被动态分配的内存段,OC对象(所有继承自NSObject的对象)就存放在堆里。
(5)栈:由编译器自动分配释放,存放函数的参数值,局部变量等值。

栈内存是系统来管理的,因此我们常说的内存管理,指的是堆内存的管理,也就是所有OC对象的创建和销毁的管理。

伴随着iOS5的到来,苹果推出了ARC(自动引用计数)技术,此模式下编译器会自动在合适的地方插入retain、release、autorelease语句,也就是说编译器会自动生成内存管理的代码,解放了广大程序猿的双手,也基本上避免了内存泄露问题,但是呢…

内存泄露的定义是:用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)。

在iOS应用中的内存泄露,原因一般有循环引用、错用Strong/copy等。

重要文章

一、Analyze—静态分析

< 返回目录

顾名思义,静态分析不需要运行程序,就能检查到存在内存泄露的地方。

使用方法:打开Xcode,command + shift + B;或者Xcode - Product - Analyze;

1、常见的三种泄露情形:

(1)创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to ‘number’ is never read。翻译一下:存储在’number’里的值从未被读取过。

(2)创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to ‘str’ during its initialization is never read

(3)调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into ‘subImageRef’。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。

贴上三种常见情形的Demo代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 /**
* 情 形 一:创建了一个对象,但是并没有使用。
* 提示信息:Value Stored to 'number' is never read
* 翻译一下:存储在'number'里的值从未被读取过,
*/
- (void)leakOne {
NSString *str1 = [NSString string];
NSNumber *number;
number = @(str1.length);
/*
说我们没有读取过它,那就读取一下,比如打开下面这句代码,对它发送class消息,就不再会有这个提示了。
当然最好的方法还是将有关number的代码都删掉,因为,你只对number赋值,又不使用,那干嘛创建出来呢。
这是一个比较常见和典型的错误,也很容易检查出来
*/
// [number class];
}

/**
* 情 形 二:创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。
* 提示信息:Value Stored to 'str' during its initialization is never read
*/
- (void)leakTwo {
NSString *str = [NSString string]; // 创建并初始化str,此时已经有一个内存单元保存str初始化的值
// NSString *str; // 这样就内存不泄露,因为str是可变的,只需要先声明就行。
// printf("str前 = %p\n",str);
str = @"ceshi"; // str被改变了,指向了"ceshi"所在的地址,指针改变了,但之前保存初始化值的内存空间还未释放,保存str初始化值的内存单元泄露了。
// printf("str后 = %p\n",str); // 指针改变了
[str class];

// 再举两个例子,同理

NSArray *arr = [NSArray array];
// printf("arr前 = %p\n",arr);
// NSArray *arr; // 这样就内存不泄露
arr = @[@"1",@"2"];
// printf("arr后 = %p\n",arr); // 指针改变了
[arr class];

CGRect rect = self.view.frame;
// CGRect rect = CGRectZero; // 这样就内存不泄露
rect = CGRectMake(0, 0, 0, 0);
NSLog(@"rect = %@",NSStringFromCGRect(rect));
}

/**
* 情 形 三:调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。
* 提示信息:Potential leak of an object stored into 'subImageRef'
* 翻译一下:subImageRef对象的内存单元有潜在的泄露风险
*/
- (void)leakThree {
CGRect rect = CGRectMake(0, 0, 50, 50);
UIImage *image;
CGImageRef subImageRef = CGImageCreateWithImageInRect(image.CGImage, rect); // subImageRef 引用计数 + 1;

UIImage* smallImage = [UIImage imageWithCGImage:subImageRef];

// 应该调用对应的函数,让subImageRef的引用计数减1,就不会泄露了
// CGImageRelease(subImageRef);

[smallImage class];
UIGraphicsEndImageContext();
}

自己遇到的实例:

情形2:静态检测内存泄露Analyze--Value stored to ‘dataArr’ during its initialization is never read
即初始化的时候开辟了一块内存,却始终没用到,导致该块内存泄漏

1
2
3
4
5
6
NSMutableArray *tempMutArr = [NSMutableArray arrayWithCapacity:0];
if ([self.clickedButtonTpye isEqualToString:KClickedButtonTypeLast]) {
tempMutArr = self.lastDataSourceArr;
}else{
tempMutArr = self.hotDataSourceArr;
}

二、Leaks—内存泄露

< 返回目录

  • MLeaksFinder:精准 iOS 内存泄露检测工具

    项目 UIViewController+MemoryLeak.m NSObject+MemoryLeak.m

    原理:MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

    即我们hook viewDidDisappear:dismissViewControllerAnimated: completion: 都去执行NSObject的willDealloc方法。

    在NSObject的willDealloc方法中,隔2秒后再去尝试调用另一个方法assertNotDealloc。如果UIViewController已经被成功释放,则肯定是 nil 执行 assertNotDealloc,即assertNotDealloc不会被执行,也就不会弹出内存泄露的弹窗。反之,若UIViewController没有释放,则会弹出内存泄露的弹窗。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    > // UIViewController+MemoryLeak.m
    > - (void)swizzled_viewDidDisappear:(BOOL)animated {
    > [self swizzled_viewDidDisappear:animated];
    >
    > if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
    > [self willDealloc];
    > }
    > }
    >
    > - (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    > [self swizzled_dismissViewControllerAnimated:flag completion:completion];
    > ......
    >
    > [dismissedViewController willDealloc];
    > }
    >
    > // NSObject+MemoryLeak.m
    > - (BOOL)willDealloc {
    > ......
    >
    > __weak id weakSelf = self;
    > dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    > __strong id strongSelf = weakSelf;
    > [strongSelf assertNotDealloc];
    > });
    >
    > return YES;
    > }
    >
    > - (void)assertNotDealloc {
    > if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
    > return;
    > }
    > [MLeakedObjectProxy addLeakedObject:self];
    >
    > NSString *className = NSStringFromClass([self class]);
    > NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
    > }
    >
  • 自动化内存泄漏检测 项目 https://github.com/liujiakuoyx/leak_detector/blob/main/lib/src/leak_navigator_observer.dart#L121

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    > ///Such as WebSocket is delay close connect.
    > const int _defaultCheckLeakDelay = 500;
    >
    > typedef ShouldAddedRoute = bool Function(Route route);
    >
    > ///NavigatorObserver
    > class LeakNavigatorObserver extends NavigatorObserver {
    > final ShouldAddedRoute? shouldCheck;
    > final int checkLeakDelay;
    >
    > ///[callback] if 'null',the all route can added to LeakDetector.
    > ///if not 'null', returns ‘true’, then this route will be added to the LeakDetector.
    > LeakNavigatorObserver(
    > {this.checkLeakDelay = _defaultCheckLeakDelay, this.shouldCheck});
    >
    > @override
    > void didPop(Route route, Route? previousRoute) {
    > _remove(route);
    > }
    >
    > @override
    > void didPush(Route route, Route? previousRoute) {
    > _add(route);
    > }
    >
    > @override
    > void didRemove(Route route, Route? previousRoute) {
    > _remove(route);
    > }
    >
    > @override
    > void didReplace({Route? newRoute, Route? oldRoute}) {
    > if (newRoute != null) {
    > _add(newRoute);
    > }
    > if (oldRoute != null) {
    > _remove(oldRoute);
    > }
    > }
    > ......
    > }
    >
  • 快手 Flutter 上的内存泄漏监控

Leaks是动态的内存泄露检查工具,需要一边运行程序,一边检测。

内存泄漏Leak1

先不切换到Call Trees,先看看Statistics(统计数据)下的情况

内存泄漏Leak2
Allocation中我们主要关注的是Persistent和Persistent Bytes,分别表示当前时间段,申请了但是还没释放的内存数量和大小。

切换到Call Trees后世这样的

内存泄漏Leak3

项目中遇到过的内存泄漏

这里我们不是去查看Call Tree,而是查看Cycles & Roots

循环引用定位1

点击标记4处为黑色,即代表该处会发生内存泄漏,双击进入代码,如图:
循环引用定位2
果然存在内存泄漏

自己遇到的其他例子:

Leak_mine_1
开始不明白为什么这个变量会内存泄漏后面才能白,其实_priceDetailModel这个本身已经用OrderPriceDetailModel赋值过一次了,而这里你又赋值了一次,导致多了一个。代码情况如下两个图:
Leak_mine_2
又在set方法中生成了一个地址
Leak_mine_3

三、ARC下的dealloc

1、ARC下dealloc的使用

ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:

1
2
3
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。
(1)、通知的观察者,或KVO的观察者;

由于通知中心是系统的一个单例,你在注册通知的观察者时,实际上是在通知中心注册的,

这时,即使ARC下系统帮我们释放了对象,但是在通知中心的观察还是没有移除,那么当有

该通知时,依然会尝试调用该对象的接受通知的方法,这可能会导致一些问题.

(2)、对象强委托/引用的解除;

对于其他的对象来把你当做委托 delegate时,并且是 强引用时,即时你自身被释放,但是引用你的对象依然还在,

这时需要在引用你的对象移除该delegate

(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。

一个对象,如一个ViewController在销毁之前有可能需要和server打交道;

这时我们也可以在dealloc中写

2、controller 不能释放,不走dealloc方法的几种可能

主要原因还是循环引用,引起的内存泄漏。

详情参考:controller 不能释放,不走dealloc方法的4种可能

四、Time Profile

< 返回目录

详情参考:instrument Time Profiler总结

使用Time Profile前有两点需要注意的地方:

1
2
1、一定要使用真机调试
2、应用程序一定要使用发布配置

图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方

TimeProfile1

其他

其他参考材料:

问:Xcode 运行程序,左侧memory 不显示内存。。

答:运行程序后,xcode 不显示当前使用的内存情况,问题是打开了僵尸–enable zoombie Objects,关闭即可。
即打开 product—>SCheme–>EditSCheme –>enable zoombie Objects 取消选中 ok

就可以继续显示了

常见笔试/面试题

< 返回目录

END

< 返回目录

内存-②循环引用

内存-②循环引用

[toc]

目录

1
2
3
4
5
6
7
8
9
10
1、Objective-C中block为何得用copy修饰,能否用其他
2、block的循环引用
3、block循环引用的解决
4、判断该block是否会发生循环引用
5、为什么masonry的block里引用self不需要weak?
6、是否所有的Block中,使用self 都会导致循环引用?
7、block修改外部局部变量
(1)、在block中无法直接修改外部变量的原因
(2)、解决如何在 block 中修改外部变量
>

一、循环引用原因

< 返回目录

导致iOS对象无法按预期释放的一个无形杀手是——循环引用。循环引用可以简单理解为A引用了B,而B又引用了A,双方都同时保持对方的一个引用,导致任何时候引用计数都不为0,始终无法释放。若当前对象是一个ViewController,则在dismiss或者pop之后其dealloc无法被调用,在频繁的push或者present之后内存暴增,然后APP就duang地挂了。

循环引用会导致内存泄露,因为循环应用会导致,有些对象没办法在已经不会再使用的时候被释放掉

下面列举我们变成中比较容易碰到的三种循环引用的情形:

1
2
3
4
5
6
(1)计时器NSTimer:✅__weak typeof(self) weakSelf = self;
循环引用:self -> timer -> block -> self
(2)block:✅copy
@property (nonatomic, copy) void (^myBlock)(void);
(3)委托delegate:✅weak
@property (nonatomic, weak) id<YourDelegateProtocol> delegate;

二、NSTimer的循环引用

< 返回目录

NSTimer的循环引用详情,我们放在下面讲解NSTimer(前面已经提过NSTimer 其实就是RunLoop中的CFRunLoopTimerRef,一个基于时间的触发器)的时候介绍。

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@interface HWWeakTimerTarget : NSObject

@property (nonatomic, weak) id target; // 注意是 weak
@property (nonatomic, assign) SEL selector; // assign
@property (nonatomic, weak) NSTimer* timer; // 注意是 weak

@end

@implementation HWWeakTimerTarget

- (void)fire:(NSTimer *)timer {
if(self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
#pragma clang diagnostic pop
} else {
[self.timer invalidate];
}
}

@end

@implementation HWWeakTimer

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
HWWeakTimerTarget *timerTarget = [[HWWeakTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
}

@end

三、委托delegate的循环引用

< 返回目录

在委托问题上出现循环引用问题已经是老生常谈了,声明delegate时请用weak(ARC),如果是MRC则用assign。千万别手贱。

四、block的循环引用

< 返回目录

1、Objective-C中block为何得用copy修饰,能否用其他

答:因为block在创建的时候,它的内存是分配在栈(stack)上,而不是在堆(heap)上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。所以,为了能够在block的声明域外也能够使用block,我们需要将block拷贝到堆上,所以使用copy属性。对于堆中的block,也就是copy修饰的block。他的生命周期是随着对象的销毁而结束的。只要对象不销毁,我们就可以调用的到在堆中的block。

Block 在创建时可能存储在不同的内存区域(栈或堆)中,而在将其赋值给对象或作为函数或方法的参数时,需要确保 Block 存储在堆中,以便在调用时能够正常执行。如果 Block 存储在栈中,则在函数或方法返回后,Block 对象所在的内存区域将被释放,从而导致程序崩溃或者产生其他未定义的行为。所以block应该被持有,即应该使用copy或者strong修改。那为什么我们还是习惯使用copy呢?因为使用 copy 可以提高线程安全性,因为每个线程可以独立地使用 block 的副本,而不必担心其他线程对原始 block 的修改。举例点击一个按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@interface MyClass : NSObject
@property (nonatomic, copy) void (^updateUIBlock)(void); // 使用 copy 属性
@end

@implementation MyClass

- (id)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self; // 使用 __weak 避免循环引用
self.updateUIBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf __updateUI];
});
}
};
}
return;
}
- (void)__updateUI {
// 更新 UI 的代码
NSLog(@"UI updated on main thread");
}

- (void)clickToUpdateUI {
// 执行异步任务,并在完成后调用 updateUIBlock
[self performBackgroundTaskWithCompletion:self.updateUIBlock];
}

// completion这个block如果是原block的副本,那么这里即使多个后台任务尝试同时更新 UI,每个任务都会有 `updateUIBlock` 的独立副本,不会相互干扰。这确保了 UI 更新操作的线程安全性。
- (void)performBackgroundTaskWithCompletion:(void (^)(void))completion {
// 模拟后台线程任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ... 执行一些后台操作 ...

// 任务完成后,调用 completion block
if (completion) {
completion();
}
});
}

@end

扩展:为什么NSString 可以使用strong和copy,却选择使用 copy?(个人理解:因为NSString是不可变的,既然不可变还不如使用copy再加一层线程安全)

使用 copy 属性修饰符通常有以下原因:

  1. 确保线程安全:如果你在多线程环境中操作字符串,使用 copy 可以确保每个线程都有自己的副本,从而避免线程安全问题。
  2. 避免外部修改:如果你不希望外部代码修改原始字符串,使用 copy 可以确保你的属性持有的是原始字符串的一个副本,外部代码对原始字符串的修改不会影响到你的属性。
2、block的循环引用

由于block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。所以,不管是在ARC还是非ARC环境下对block使用不当都会引起循环引用问题

一般表现为,某个类将block作为自己的属性变量(则该类就对block强引用了),然后该类在block的方法体里面又使用了该类本身。即当对象(比如self)拥有一个block属性的时候,在block属性中又引用了对象的其他成员变量或者调用了对象的其他方法。形成你中有我,我中有你,谁都无法将谁释放的困局。形如:

1
2
3
4
5
6
7
8
9
10
11
self.myBlock = ^{
[self doSomething];
};

又或者

ClassA* objA = [[ClassA alloc] init];
objA.myBlock = ^{
[self doSomething];
};
self.objA = objA;

block的这种循环引用会被编译器捕捉到并及时提醒。

以上参考自:Block的循环引用

3、block循环引用的解决
(1)、常规简单解法

解决方法,就一句话的事情:

1
2
3
4
5
6
7
8
__weak typeof (self) weakSelf = self; 
self.myBlock = ^{
[weakSelf doSomething];
};

附1:如果是non-ARC环境下就将__weak替换为__block即可。non-ARC情况下,__block变量的含义是在Block中引入一个新的结构体成员变量指向这个__block变量,那么__block typeof(self) weakSelf = self;就表示Block别再对self对象retain啦,这就打破了循环引用。

附2:__weak 是 iOS 5.0 推出的,_weak 相当于 weak,不会做强引用,如果对象被释放,执行的地址,会指向 nil
(2)、block中使用 weak–strong dance 技术避免循环引用
1
2
3
4
5
6
7
8
__weak typeof(self)weakSelf = self;
[header setTapHandle:^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"strongSelf = %@", strongSelf);
}
[weakSelf headerAction:header];
}];

精髓2:①在 block 之前定义对 self 的一个弱引用weakSelf,因为是弱引用,所以当 self 被释放时weakSelf会变为nil;② 在 block 中引用该弱应用,考虑到多线程情况,通过使用强引用 strongSelf 来引用该弱引用,这时如果 self 不为 nil 就会 retain self,以防止在后面的使用过程中 self 被释放;③在之后的 block 块中使用该强引用 bself,注意在使用前要对 bSelf 进行了 nil 检测,因为多线程环境下在用弱引用weakSelf对强引用strongSelf赋值时,弱引用weakSelf可能已经为nil了。通过这种手法,block 就不会持有 self 的引用,从而打破了循环引用。

iOS开发中在block中为什么要weak和strong配合使用

答:weak是为了解决循环引用。strong是为了防止block持有的对象提前释放。

1
2
3
4
5
6
7
8
9
10
11
 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self dismissViewControllerAnimated:YES completion:nil];

__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf);
});
};
self.block();
}

点击屏幕,当前控制器消失,同时被销毁掉,5秒后打印的weakSelf就是一个(null)。
而我们如果在block内使用__strong后就能保证再打印完strongSelf之后再释放当前控制器。

4、判断该block是否会发生循环引用

判断该block是否会发生循环引用例子

5、为什么masonry的block里引用self不需要weak?

这个就和网络请求里面使用self道理是一样的。因为UIView未强持有block,所以这个block只是个栈block,而且构不成循环引用的条件。栈block有个特性就是它执行完毕之后就出栈,出栈了就会被释放掉。看mas_makexxx的方法实现会发现这个block很快就被调用了,完事儿就出栈销毁,构不成循环引用,所以可以直接放心的使用self。

1
2
3
4
5
6
7
8
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

如果要强引用,block应该是masnory的一个属性,即被masnory对象持有。且是copy修饰符

6、是否所有的Block中,使用self 都会导致循环引用?

7、block修改外部局部变量

如果是全局变量呢?

(1)、在block中无法直接修改外部变量的原因

错误示例:

1
2
3
4
5
6
7
8
9
10
11
//声明一个局部整型变量 
int intValue = 3; //漏掉了__block修饰符

//声明一个返回值为int,一个int参数的block变量
int (^block)(int) = ^(int m){
intValue++;
return m * intValue;
};

//调用block变量,5作为参数之后的结果
NSLog(@"block(5) = %d",block(5));

在上面的例子中,我们编译程序后发现编译器会有红色错误,错误提示为
Variable is not assignable (missing __block type specifier)

为什么会出现不能被赋值的错误提示呢?

block在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,在 block 块内使用该只读拷贝。
那为了避免上述错误,就要精髓1:使用__block修饰符来修饰外部变量,用来通知编译器该外部变量intValue与block中的intValue指的是同一块儿内存地址,而不需要内存拷贝。

(2)、解决如何在 block 中修改外部变量

有两种办法
① 第一种是可以修改 static 全局变量;
② 第二种是可以修改用新关键字 __block 修饰的变量。

1
2
3
4
5
6
7
8
9
10
11
12
__block int blockLocal  = 100;
static int staticLocal = 100;

void (^aBlock)(void) = ^(void){
NSLog(@" >> Sum: %d\n", global + staticLocal);

global++;
blockLocal++;
staticLocal++;
};

aBlock();

附:静态变量 和 全局变量 在加和不加 __block 都会直接引用变量地址。也就意味着静态变量和全局变量的修改可以直接修改,不需要作添加__block的步骤。

在ARC下获取对象的引用计数值

在ARC下获取对象的引用计数值,可以使用CFGetRetainCount。

1
2
3
4
5
6
7
对于Core Foundation对象:
NSLog(@"CFGetRetainCount is %ld", CFGetRetainCount(aCFString));

对于Foundation对象
NSLog(@"CFGetRetainCount is %ld", CFGetRetainCount((__bridge CFTypeRef)aNNstring));

NSLog(@"%@ CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));

以下获取引用计数的方法错了,但是不知道正确的应该怎么用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)dealloc {
NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];

NSLog(@"%@ viewWillDisappear and CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));
}

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];

NSLog(@"%@ viewDidDisappear and CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));
}

几篇文章:

常见笔试/面试题

< 返回目录

block和weak修饰符的区别是什么?
1
2
3
4
1,在MRC时代,__block修饰,可以避免循环引用;ARC时代,__block修饰,同样会引起循环引用问题;
2,__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型;
3,__weak只能在ARC模式下使用,也只能修饰对象,不能修饰基本数据类型;
4,__block对象可以在block中被重新赋值,__weak不可以;

END

< 返回目录