新电脑(Mac)初始安装

科学上网

详见:常识类/技术常识/科学上网_ClashX

必备AI

1、OpenCode、Codex

  • opencode 终端/App :免费,开箱即用

    [!TIP]

    opencode 的使用教程

    Zen 的工作方式与 OpenCode 中的任何其他提供商相同。你登录 OpenCode Zen 并获取 API 密钥。它是完全可选的,即使不用它,你也可以照常使用 OpenCode。(若要Zen的API key ,登录 **OpenCode Zen**,添加你的账单信息,然后复制你的 API 密钥。)

    你可能会在使用记录中看到 Claude Haiku 3.5。这是一个低成本模型,用于生成会话标题。

    免费模型:

    • DeepSeek V4 Flash Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
    • MiniMax M2.5 Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
    • Ring 2.6 1T Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
    • Nemotron 3 Super Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
    • Big Pickle 是一个隐身模型,目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
  • Codex App:需付费购买apikey

  • GeminiCLI

    Gemini Api key

    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
    brew install gemini-cli
    gemini

    export GEMINI_API_KEY="YOUR_API_KEY"
    # 或
    echo 'export GEMINI_API_KEY="你的真实API密钥"' >> ~/.zshrc
    source ~/.zshrc

    echo $GEMINI_API_KEY

    # 测试代理是否正常工作:
    # 如果能返回响应(哪怕是错误码),说明代理可用
    # 如果报连接拒绝或超时,说明代理端口不对或代理软件没运行
    curl -x http://127.0.0.1:7897 https://generativelanguage.googleapis.com -I

    # 方法一:用调试模式运行,看详细错误
    export DEBUG=1
    gemini -p "你好" 2>&1 | head -50
    # 方法二(推荐):测试 API Key 是否有效(独立于 CLI)
    curl -X POST "https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"contents":[{"parts":[{"text":"你好"}]}]}'


    # 额度超过,就使用账号登录
    # 1. 清除API Key配置
    rm ~/.gemini/settings.json
    # 2. 清除环境变量避免冲突
    unset GEMINI_API_KEY
    # 3. 重新运行,选择"Login with Google"
    gemini

2、模型切换工具

3、Agent SKILL

按 [《AI-①Agent & SKILL.md》](../AI/AI-①Agent & SKILL.md) 安装SKILL

一、系统设置

  • mac三指拖动设置

    1
    系统设置 --> 辅助功能 --> 鼠标与触控板 --> 触控板选项 --> 三指拖移
  • mac显示隐藏文件

    1
    defaults write com.apple.finder AppleShowAllFiles -boolean true;killall Finder

二、搜狗输入法、QQ、微信、Wps

三、琐事记录&计划

1、开始记录各种琐事:个人博客Git

2、找到对应的文件/应用:Alfred、QSpace、Manico、Magnet

3、编辑:Typora、SublimeText、截图Xnip、翻译Bob、粘贴Paste

4、列计划:omniplan、Xmind

5、其他:Bartender

6、switchhosts

switchhosts:切换多个 hosts 方案的工具

四、代码相关

1、Sourcetree、gitlab、github项目、ssh

github个人创建的token,仅能支持个人下的代码推送,而不支持 github 的其他组织的代码推送。所以对于github组织的repo下载请使用 https ,不用要 ssh 。因为个人的和组织的token不共用。

github个人创建的token,仅能支持个人下的代码推送,而不支持 github 的其他组织的代码推送。所以对于github组织的repo下载请使用 https ,不用要 ssh 。因为个人的和组织的token不共用。

git clone的时候遭遇fatal: early EOF fatal: index-pack failed解决办法

一般这种情况都是因为项目分支过多,导致你要下载的东西太多,从而引起这个问题。

1
2
3
4
5
6
7
8
9
10
11
❌无效解法:
网上有一个流传比较多的修改方案:
git config --global core.compression -1
使用上述命令,来修改压缩模式,我尝试过了没有用。

✅有效解法:
分析因为引起这个问题的根源是文件过多,所以我们可以分批次下载文件,先下载一部分,再下载剩下的。以下就是我的解决方案:
git config --global core.compression 0 # 1、关闭 core.compression
git clone --depth 1 url xxx本地库名 # 2、然后使用depth这个指令来下载最近一次提交
cd xxx本地库名 && git fetch --unshallow # 3、然后获取完整库
git pull --all # 4、最后pull一下查看状态,问题解决
  • Sourcetree

    让 SourceTree 走代理

    1、为 Git 配置代理(适用于 HTTPS 仓库地址)

    在终端窗口中,输入下面两行命令,每输入一行后按一次回车请务必将 7897(很多软件默认是 7890) 替换成你代理软件(如 Clash、V2Ray、Shadowsocks 等)上显示的 HTTP 代理端口。

    1
    2
    3
    4
    5
    git config --global http.proxy http://127.0.0.1:7897
    git config --global https.proxy http://127.0.0.1:7897

    #(命令执行后不会有任何提示,表示已成功写入配置。)
    # 验证方法:设置完成后,在 SourceTree 里正常执行 `fetch`、`pull` 或 `push` 操作。如果不再报网络错误,就说明配置成功了。
  • 生成ssh

    1
    2
    3
    4
    5
    6
    7
    # 查看是已有
    cd ~/.ssh

    # 没有则执行生成
    ssh-keygen

    # 生成后,拷贝id_rsa.pub中的秘钥到git的ssh配置中
  • gitlab

  • github

    mac中用sourcetree下载clone代码,结果出错:warning: templates not found in /usr/local/git/share/git-core/templates

    image-20201103134013411

    解决办法:

    1
    2
    sudo mkdir -p /usr/local/git/share/git-core/templates
    sudo chmod -R 755 /usr/local/git/share/git-core/templates

    image-20201103135013020

2、Cocoapods、Spec

  • Cocoapods官网

    1、安装Cocoapods

    1
    2
    3
    gem list cocoapods # 查看是否安装

    sudo gem install cocoapods

    2、查看我的指定邮箱账号下有哪些pod

    1
    2
    3
    4
    5
    6
    7
    # 查看我的指定邮箱账号下有哪些pod
    pod trunk me

    # 新电脑或者切换要查看的邮箱 需要注册并验证你的设备
    pod trunk register 你要查询的邮箱 '你的用户名' --description='任意描述'

    pod trunk register dvlproad@163.com dvlproad --description='qian_mac_m5'

3、go2sheell

五、iOS编码工具

1、Xcode

2、CodeSnippets

1、进入 BlackMagic Git工程中的 CodeSnippets 并下载

2、CodeSnippets所在位置

①自己定义的代码块是在以下目录下:
~/Library/Developer/Xcode/UserData/CodeSnippets/

3、iSwift破解版

4、其他 cocoapods APP

5、沙盒 XSimulatorMng

沙盒数据查看.md

六、Android

1、Android Studio

七、WebStrom

1、WebStrom APP

2、WebStorm细节配置

2.2、WebStorm配置直接运行RN项目

    具体操作请查看:[ReactNative开发环境搭建与运行](../../ReactNative/1入门/ReactNative开发环境搭建与运行.md)

七、AI 智能编程助手

AI编程助手 来源 官网 备注 推荐指数
codegeex 清华 https://codegeex.cn/ ⭐️⭐️⭐️⭐️⭐️
Codeium 初创公司 https://codeium.com/ 开源:https://github.com/THUDM/CodeGeeX
CodeWhisperer 亚马逊
image-20240317232514136

八、抓包 Charles

官网使用30天。请进入百度网盘下载 5.0.1 破解版 或进入 https://xclient.info/ 搜索下载最新版

  • Charles 下载

    进入百度网盘,搜索Charles,目前实用4.5.5版本

    Charles

  • Charles.md

九、brew、yarn、npm

  • brew

    1
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  • yarn

    1
    brew install yarn
  • npm

  • wrap

十、个人娱乐类

十一、网站

1、APP网站及APP安装问题解决方法

详见:常识类/技术常识/好用的网站

十二、其他小工具

详见:实用工具/其他小工具

其他、软件安装问题

1、已损坏

1
sudo xattr -r -d com.apple.quarantine xxx.app

内存-①内存基础

# 目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
1、内存管理
(1)、在ObjC中,对象什么时候会被释放(或者对象占用的内存什么时候会被回收利用)?
(2)、那怎么知道对象已经没有被引用了呢?
(3)、autorelease、autoreleasepool(自动释放池)
(4)、autoreleasepool(自动释放池) 
(5)、autoreleasepool里面对象的内存什么时候释放?
(6)、runloop、autorelease pool以及线程之间的关系
(7)、自动释放池怎样创建
(8)、自动释放池使用注意
(9)、自动释放池的应用/什么时候要用@autoreleasepool

2、如何监测内存泄漏
1
自动释放池底层怎么实现?

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

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
11
12
- (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

第1节:ReactNative开发环境搭建与运行

ReactNative

一、检测您是否具备了ReactNative的开发环境

1、环境已完善时

2、环境未完善时

2.1、React Native 环境搭建

二、创建第一个ReactNative项目

1、创建项目

方法一:执行react-native init cj_rn_logindemo

此步骤会生成**React Native依赖包node_modules**文件夹。每个项目中都要有这部分内容。且该部分的内容可以直接被复制使用。所以,创建项目还有另一个方法。

生成的cj_rn_logindemo工程详见cj_rn_logindemo

方法二:原有iOS项目集成RN

参考来源:React Native 集成到原生项目(iOS)

react-native 指定版本创建项目

因为react-native更新太快,每次react-native init初始化项目的时候,都会安装最新的rn版本,导致之前的一些api不兼容等问题。
所以我们需要安装指定版本的rn来初始化项目:

1
react-native init demo --version 0.59.8
1
2
3
4
5
6
7
react-native 安装指定的版本

react-native init name --version 0.xx.xx

react-native 降低版本

react-native --save [react-native@0.xx.xx](mailto:react-native@0.xx.xx)

三、运行ReactNative项目

1、运行方法

1.1、常见运行方法

命令行运行项目

进入项目根目录cd TestRNProject

运行iOS项目react-native run-ios

1.2、像iOS原生那样在Xcode中运行

双击ios/TestRNProject.xcodeproj文件然后在Xcode中点击Run按钮。(最常用,对iOS开发者)

1.3、在WebStorm中直接运行

详见本文中的附一、进阶:为WebStorm搭建React Native开发环境

1.4、Android中找不到devices的问题处理

错误提示:error No Android devices connected.

rn_Android_device

此时需要:

①、adb kill-server

②、adb start-server

原因:模拟器在运行一段时间后,adb服务有可能会出现异常。这时需要重新对adb服务关闭和重启。

附:reload 命令adb shell input keyevent 82

2、运行项目的常见问题

2.1、RN项目报错“RCTBundleURLProvider.h” file not found

从网上下载别人的ReactNative项目,打开iOS项目的时候,xcode会报错,提示:“RCTBundleURLProvider.h” file not found

原因:node_modules文件夹不存在 或者 node_modules文件夹下的包和当前版本不匹配

解决方法:
1、打开Mac里面的终端,进入项目所在的文件夹目录;
2、把项目里面的 node_modules 文件夹删除掉,然后执行 npm install 命令
3、npm install安装完成后, 执行react-native upgrade命令
4、最后重新打开Xcode,clean一下,应该就没有问题了。

附一、进阶:为WebStorm搭建React Native开发环境

PS:最近又回来搞RN了,以下虽然都是一些老知识了,但你也许有用

问题背景:我不想在WebStorm/VSCode、终端、XCode中来回切换。我想在WebStorm直接运行ReactNative项目
问题背景:我不想在WebStorm/VSCode、终端、XCode中来回切换。我想在WebStorm直接运行ReactNative项目
问题背景:我不想在WebStorm/VSCode、终端、XCode中来回切换。我想在WebStorm直接运行ReactNative项目

1、为Run添加Add New Configuration

image-20190519205612140

点击,Edit configurations…会进入如下界面

image-20190519221955691

点击 + ,选择npm

image-20190519222125672

2、完善所添加的Configuration内容

外层内容填写如下:

image-20190519222852727

External tool内容如下:

image-20190519223454054

上述步骤2中的各值分别为:

  • Program:

    which react-native中获得的 /usr/local/bin/react-native

    image-20190519223703388

  • Arguments:

    run-ios --simulator="iPhone Xʀ"

    run-ios --device="iPhone7_Qian"

    run-android --deviceId="emulator-5554"

附:启动和关闭ADB服务(adb start-serveradb kill-server

iOS设备值来源:

设备_iOS

安卓设备值来源:

设备_adb devices

执行的命令有:

注:前提要cd到工程中后,执行react-native run-android --help才有效。

设备_react-native run-android  –help

  • Working directory:

    建议通过如下图添加宏的方式添加Working directory值。

    image-20190519224816841

添加完成后的图为:

image-20190519225336448

至此,配置结束。接下来我们试下效果。

恭喜,配置结束,休息一下吧

3、试下配置效果是否有效

Run菜单中选择刚才添加的执行项,其就会在执行我们所设置的选项,即执行/usr/local/bin/react-native run-ios "--simulator=iPhone Xʀ"

执行结果如下:

image-20190519225942964

同样的其也会在终端中有如下显示:

image-20190519231236234

当然,如果你使用快捷键”Cmd+R”也能达到这个效果。

image-20190519231502977

经验证,配置无误,使用有效。

经验证,配置无误,使用有效,打工告成

四、Xcode11问题

2019.10.27: Could not find iPhone 11 simulator. Run CLI with –verbose flag for more details.

1、初步分析:

Xcode 11无法在模拟器上运行React-Native应用程序。看结论为没有你想运行的那个模拟器iPhone X。
2、判断分析正确否:

单独运行模拟器,在顶部菜单中,在“硬件,设备,iOS 13.0”下查看。

rn_Could not find simulator

①当您执行run-ios时,react-native旨在匹配请求的设备。内部硬编码默认值为iPhoneX。

②尝试匹配请求的设备的函数在:

/node_modules/@react-native-community/cli-platform-ios/build/commands/runIOS/findMatchingSimulator.js

设计此功能是为了使您可以为其提供设备和可选的版本号。

③如果找不到给定的设备和版本,则默认情况下它将使用列表中的第一个设备返回匹配项。

3、解决:

如上所述,首先自己运行模拟器,并记下所需的iPhone或iPad。

然后将此名称作为可选参数传递给run-ios命令行命令,如下所示:

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)数据清理

清理项

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

  • 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等)

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

Mac电脑磁盘太高不够用,怎么清理?

清理 “系统数据”

1、删除 Xcode 旧版本文件:可以全清空

1
open ~/Library/Developer/Xcode/DerivedData

2、清理 iOS 模拟器设备

1
2
3
4
5
# 查看
open ~/Library/Developer/CoreSimulator

# 删除所有已关闭(Shutdown)的模拟器
xcrun simctl delete unavailable

删除旧的 iOS 模拟器数据。

内存-①AutoRelease

内存

必看文章:

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

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

说说内存管理

我们知道,当不再使用一个对象时应该将其释放,但是在某些情况下,我们很难理清一个对象什么时候不再使用(比如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里面对象的内存什么时候释放?

@autoreleasepool 的释放时机 取决于它所在的作用范围和运行环境,通常有以下几种情况:


1. 手动创建的 @autoreleasepool 何时释放?

当 @autoreleasepool 代码块执行完毕时,自动释放池会立即释放其中的对象。

示例:

1
2
3
4
5
for (int i = 0; i < 1000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"Test %d", i]; // 自动加入 AutoreleasePool
} // 离开作用域,AutoreleasePool 立即释放
}

📌 释放时机

离开 @autoreleasepool {} 作用域后,立即释放


2. RunLoop 维护的 AutoreleasePool 释放时机

主线程有 RunLoop 的线程,AutoreleasePool 默认由 RunLoop 维护,它的释放时机与 RunLoop 的生命周期相关。

🔹 RunLoop 相关的释放时机

iOS 的 每个 RunLoop 周期 都会创建和销毁一个 AutoreleasePool,通常发生在:

​ 1. RunLoop 进入休眠(BeforeWaiting) 👉 释放旧的 AutoreleasePool

​ 2. RunLoop 唤醒(AfterWaiting) 👉 创建新的 AutoreleasePool

📌 释放时机

​ • kCFRunLoopBeforeWaiting(即将进入休眠)

​ • kCFRunLoopExit(即将退出 RunLoop)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)runLoopObserver {
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
YES, // 监听所有循环
0, // 优先级
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopBeforeWaiting) {
NSLog(@"RunLoop 进入休眠前,AutoreleasePool 释放");
} else if (activity == kCFRunLoopExit) {
NSLog(@"RunLoop 退出前,AutoreleasePool 释放");
}
}
);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
}

​ • 主线程 默认跟随 主 RunLoop,每次 事件处理完毕后 释放 AutoreleasePool。

​ • 子线程 如果没有手动创建 RunLoop,则不会自动创建 AutoreleasePool。


4. 为什么 @autoreleasepool 需要释放?

iOS 对象默认使用引用计数(ARC 或 MRC),但 autorelease 标记的对象不会立即释放,而是存放在 AutoreleasePool 中,等待合适的时机释放。如果 AutoreleasePool 释放不及时,可能会导致大量内存占用,甚至崩溃(OOM)。

常见问题:

大量 autorelease 对象未释放

1
2
3
for (int i = 0; i < 1000000; i++) {
NSString *str = [NSString stringWithFormat:@"Memory leak %d", i];
}

使用 @autoreleasepool 限制内存增长

1
2
3
4
5
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"Optimized %d", i];
}
}

💡 这样做的好处

​ • 避免 一次性分配大量内存,减少内存峰值。

​ • 提高 内存回收效率,防止 OOM(内存溢出)。


🔹 总结

​ 1. 手动 @autoreleasepool:作用域结束后立即释放。

​ 2. 主线程 AutoreleasePool:跟随 RunLoop,每次 BeforeWaiting 释放旧池,AfterWaiting 创建新池。

​ 3. 子线程 AutoreleasePool

​ • 如果 没有 RunLoop 👉 需要手动创建 @autoreleasepool

​ • 如果 有 RunLoop 👉 跟随 RunLoop 释放(BeforeWaiting / Exit)

​ 4. 合理使用 @autoreleasepool 可优化内存占用,防止 OOM。

  在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

修饰词的作用

个人觉得是为了对各种对象进行内存管理而产生的。

Objective-C 中的 assign、strong、weak、copy 等修饰词(属性关键字)主要是为了解决 iOS 内存管理(Memory Management)

1、内存的存储位置(堆、栈、全局区、常量区)

1.1、所有 alloc, new, copy, mutableCopy 创建的对象都会存储在堆上

1
2
3
4
UIViewController *vc = [[UIViewController alloc] init];
• vc 是栈上的指针,指向堆上的对象。
• 这个 UIViewController 实例存储在堆上,并且受**引用计数(ARC/手动管理)**控制。
• 只有当 vc 及其所有强引用都释放时,该对象才会被销毁。

1.2、基本数据类型(如 int、float、char、BOOL、double)通常存储在栈上

1
2
3
4
5
6
7
8
9
void testFunction() {
int a = 10; // a 存在栈上
int b = 20; // b 存在栈上
} // 函数执行完毕后,a 和 b 自动销毁
• a 和 b 直接存储在栈上,因为它们的大小固定,生命周期由作用域决定。
• 当函数执行完毕后,栈上的变量会自动释放。

int globalVar = 100; // 存在全局区
static int staticVar = 200; // 存在全局区

但如果基本数据类型是作为对象的属性,且对象在堆上,那么它们也在堆上

1
2
3
4
5
6
7
8
@interface MyObject : NSObject
@property (nonatomic, assign) int age;
@end

MyObject *obj = [[MyObject alloc] init];
obj.age = 25;
• obj 是栈上的指针,指向堆上的 MyObject 实例。
• age 是 obj 的成员变量,它作为 MyObject 的一部分,也存储在堆上。

1.3、普通 NSString 在常量区

1
2
3
4
NSString *str = @"Hello, Objective-C!";
• str 是一个指针变量,如果它是局部变量,那么它存储在栈上。
@"Hello, Objective-C!" 这个字符串是Objective-C 的字符串字面量,存储在常量区(只读数据段),不会存储在堆上。
• str 只是一个指向 @"Hello, Objective-C!" 的指针,不影响该字符串的存储位置。

堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)testBlock {
void (^myBlock1)(void) = ^{
NSLog(@"Hello, World!");
};
self.myBlock = myBlock1;

UIViewController *vc = [[UIViewController alloc] init];
}

• myBlock1 是存储在栈上
• vc 是栈上的局部变量。
• vc 指向堆上的 UIViewController 实例。

myBlock1 和 vc 都会在方法结束时候释放掉,因为他们是栈上的,内存由栈管理,而不是引用计数。
所以为了避免 block 的销毁,所以我们需要使用 copy 将 block 从栈上拷贝一份到堆上。( • alloc, new, copy 创建的对象:这些对象通常是 分配到堆上 的。)

2、内存的管理

Strong: 创建的时候引用计数即为1,后续该对象本其他对象持有的话则引用计数会再增加1,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;

- (void)checkRetainCount {
Person *person = [[Person alloc] init]; // (1) person 的引用计数 = 1
Person *temp1 = person; // (2) temp1 仅仅是指针赋值,不增加引用计数
self.person1 = person; // (3) self.person1 strong 持有 person,引用计数 +1,变为 2
self.person2 = person; // (4) self.person2 strong 持有 person,引用计数 +1,变为 3
} // (5) 方法结束,局部变量 person 和 temp1 作用域结束,ARC 自动释放 person,引用计数 -1,变为 2

// 问:person 、temp1 、 person1 、 person2 这几个的引用计数是分别是多少。
// 答:它们都指向 person 这个对象,所以最后的引用计数都是一样的,都是那个对象的引用计数,在方法执行最后前是3,放完执行完后事2

// 问:
// 答:退出页面的时候,路由栈等不持有self的时候,会

delloc/引用计数怎么变为0的?

页面退出(pop、dismiss)的时候,路由栈不再持有页面self。从而页面self的retainCount==0,继而触发self的delloc(请记住是这个因果关系)。
delloc是self的引用计数为0而触发的,而不是self被销毁而触发的或者说delloc的时候self还未被真正销毁,所以你可以在delloc中调用self。那self的真正销毁是self的delloc执行完之后(在其内部调用super delloc后)。

1、self触发delloc的时候,会依次对持有的每个数据(person1、person2)进行relase操作。如果release后person对象的引用计数为0,则person也会被释放。如果person还被其他引用,如UserManager则只是引用计数减去1而已。(题外话:self的释放关注的是self自己被不被其他对象持有,而不是self持有的对象是否全都被释放)

2、因为self执行delloc的时候self并未被真正销毁,所以当页面退出的时候,如果timer的block中又未持有self的时候(一般用weakSelf),你可以在delloc中执行 [timer invalidate] 来将 timer 从Runloop中移除。

1
2
3
4
- (void)delloc {
[self.timer invalidate];
self.timer = nil; // 不做此步的话,timer这个指针仍然指向之前的内存地址。在ARC下不会有问题。但如果是MRC则做完[self.timer release]后,不做self.timer = nil 的操作,继续调用self.timer 的其他方法会导致野指针问题。即指针访问了一块已经释放的内存。指针内容请看 《2内存-①基础.md》 未初始化的指针不是空指针,而是野指针。
}

delloc流程代码示例见: https://github.com/dvlproad/CJOptimizeProject/blob/main/CJOptimizeProject/TSDemo_Optimize/Leak/TSDellocViewController.m

为什么weak能避免循环引用?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface ViewController : UIViewController
@property (nonatomic, strong) ChildViewController *childVC;
@end

@interface ChildViewController : UIViewController
@property (nonatomic, strong) ViewController *delegate; // ⚠️ strong delegate
@end

- (void)viewDidLoad {
[super viewDidLoad];

self.childVC = [[ChildViewController alloc] init]; // self 持有 childVC,childVC 的 retainCount = 1
self.childVC.delegate = self; // 如果delegate使用strong,则childVC 强制持有 self,则self 的 retainCount = 2,而退出页面的时候路由栈只对self的retainCount-1,剩下的还有1,导致self无法释放,除非你在disappear中再对self==nil,但会有其他问题。
// 而如果delegate使用 weak , 则执行self.childVC.delegate = self;时候不会改变self的引用计数,其仍为1,也就能在页面退出时候变为0,从而触发delloc。
}

使用修饰符来解决内存问题,只使用weak和strong不够吗?为什么还要assign和copy等?

非对象类型(如 int、float)根本没有引用计数的概念,它们是值类型,不是引用类型。所以在设计上增加

新的修饰符来处理更合适。而其他的weak、strong、copy等都有引用计数的概念,只是会不会改变引用计数而已。

另外在 Objective-C 中:

​ • 对象类型(NSObject 及其子类)是存储在堆(Heap)上的指针,需要 ARC 进行引用计数管理。

​ • 非对象类型(int、float、BOOL 等)是存储在栈(Stack)或者全局数据区,不需要 ARC 进行管理。

而copy修饰符主要是为了功能的考虑,让其拷贝对象,保证内容不变,即让属性存储的是原始值的拷贝,而不是原来的引用。防止可变对象被修改。不然操作的时候可能不小心操作。

关键字 适用场景 是否增加 retainCount 备注
assign 基本数据类型、struct、C 指针 ❌ 不会 适用于 int、float、BOOL、struct 等
strong 默认对象引用 ✅ 会 适用于普通对象
weak 避免循环引用 ❌ 不会 对象释放后自动置 nil,适用于 delegate、IBOutlet 等
copy 不希望对象内容被修改 ✅ 会 适用于 NSString、NSArray、NSDictionary 等

🚨 问题:多个线程对不同属性进行读写

✅ atomic 只能保证单个属性的 getter/setter 是线程安全的。⚠️ 但 atomic 并不是真正的多线程安全!

多个属性的操作(如 firstName 和 lastName 一起更新)不会自动是原子性的,会导致数据不一致。

🛠 解决方案:使用 @synchronized、GCD dispatch_barrier_async 等手段确保完整性。

🔍 线程 A & 线程 B 的并发访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Person : NSObject
@property (atomic, strong) NSString *firstName;
@property (atomic, strong) NSString *lastName;
@end



Person *person = [[Person alloc] init];
// 线程 A 修改 firstName 和 lastName
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
person.firstName = @"Alice";
person.lastName = @"Smith";
});

// 线程 B 读取 firstName 和 lastName
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@ %@", person.firstName, person.lastName);
});

⚠️ 可能出现的问题

​ • 线程 B 读取 firstName 时,线程 A 可能只完成了 firstName = @”Alice”,但 lastName 还未更新,此时打印可能会是:

1
Alice (null)

🛠 解决方案:将整个过程加锁,使得整个赋值操作是线程安全的,不会出现部分属性更新的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方法1: 通过 @synchronized 使多个属性的操作成为原子性**
- (void)setFullNameWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
@synchronized (self) {
_firstName = firstName;
_lastName = lastName;
}
}

// 方法2:使用 GCD 的 dispatch_barrier_async 保证在并发队列中,多个属性的操作作为一个整体执行,不会被其他线程打断。
dispatch_queue_t queue = dispatch_queue_create("com.example.personQueue", DISPATCH_QUEUE_CONCURRENT);
- (void)setFullNameWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
dispatch_barrier_async(queue, ^{
_firstName = firstName;
_lastName = lastName;
});
}

一、Copy

修饰符 作用 返回类型
copy 复制为 不可变对象 NSString、NSArray、NSDictionary
mutableCopy 复制为 可变对象 NSMutableString、NSMutableArray、NSMutableDictionary
  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;

添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。原因:copy修饰符代表赋值结果是要返回一个不可变的值,即是NSArry类。

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

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

把NSArray用strong修饰,会导致NSArray可能会被修改。

把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
>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框架在程序运行的时动态生成子类属性

内存-②Block

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 修饰符作用?

内存-②循环引用

# 目录
1
2
3
4
5
6
7
8
9
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 中修改外部变量
  • 有必要了解:iOS中__block 关键字的底层实现原理

  • 结论:block可以访问外部变量,但是无法修改外部变量的值,如果要修改外部变量的值,需要对外部变量加上__block作为该变量的修饰

来源:OC学习笔记之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 > [< 返回目录](#目录)