内存管理解析

目录

1.内存区域解析
2.什么是引用计数(retainCount)
3.什么是指针和地址
4.内存泄漏、野指针、空指针、僵尸对象
5.内存管理原则
6.常用内存修饰词
7.alloc、init、new、dealloc 区别
8.强引用、弱引用、循环引用
9.weak详解
10.深浅拷贝理解

https://blog.csdn.net/z119901214/article/details/82417153?spm=1001.2014.3001.5502

iOS内存管理

1.内存区域解析

常说的内存有五大区,分别为:堆区、栈区、全局区(静态区)、常量区、代码区,具体如下图。

补充说明:
1.内容分为虚拟内存+物理内存,正常初始化空间分配的都是虚拟内存,初始化实例才会占用物理内容。Instrument 的 Allocations 工具可以监控,All Heap & Anoymous VM 代表虚拟内存 Dirty Memory,resident Size 就是物理内存的大小。
2.访问内存其实都是访问的逻辑地址,需要转换后才能访问物理内存, CPU根据逻辑地址通过界限寄存器判断是否越界,越界地址错误,反之加上基址寄存器转换成物理内存地址。
3.iOS 内存中的对象主要有两类:
一类是值类型;比如 int、float、struct 等基本数据类型,值类型会被放入栈中,遵循先进后出的原则。
一类是引用类型;也就是继承 NSObject 类的所有 OC 对象。引用类型会放在堆中,当给对象分配内存空间时,对随机在内存中开辟空间。

  • iOS 内存过多而被 Kill 掉?基于什么原则?

iOS 使用的是低内存处理机制 Jetsam,基于优先级队列的机制。 当内存过低的时候,就会在队列中进行广播,希望大家尽量释放内存,如果一段时间后,仍然内存不够,就会开始 Kill 进程,直到内存够用。

  • 其他相关补充说明:

1.RAM:运行内存,不能掉电存储。ROM:存储性内存,可以掉电存储,例如内存卡、Flash。由于RAM类型不具备掉电存储能力即一掉电数据消失,所以app程序一般存放于ROM中。RAM的访问速度要远高于ROM,价格也要高。

2.App程序启动:App程序启动,系统会把开启的那个App程序从Flash或ROM里面拷贝到内存(RAM),然后从内存里面执行代码。另一个原因是CPU不能直接从内存卡里面读取指令(需要Flash驱动等等)。

3.在iOS中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的;系统使用一个链表来维护所有已经分配的内存空间(系统仅仅纪录,并不管理具体的内容);变量使用结束后,需要释放内存,OC中是根据引用计数为0,就说明没有任何变量使用该空间,那么系统将直接收回;当一个app启动后,代码区,常量区,全局区大小已固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这两个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(即是野指针报错)。

4.iOS是基于UNIX、Android是基于Linux的,在Linux和unix系统中,内存管理的方式基本相同;Android应用程序的内存分配也是如此。除此以外,这些应用层的程序使用的都是虚拟内存,它们都是建立在操作系统之上的,只有开发底层驱动或板级支持包时才会接触到物理内存。举例:在嵌入式Linux中,实际的物理地址只有64M甚至更小,但是虚拟内存却可以高达4G。

  • 线程中栈与堆是公有的还是私有的?

1.在多线程环境下,每个线程拥有一个栈和程序计数器,栈和程序计数器用来保存线程执行历史和线程执行的状态,是线程私有资源。
2.堆资源是统一进程内多线程共享的。

内存五大区
内存区域分布
这个代码区存放于低地址,栈区存放于高地址,区与区之间并不是连续的。栈区:程序猿不需要管理栈区变量的内存,栈区地址从高到低分配;堆区:内存分配使用的是alloc,需要程序猿管理内存,堆区的地址是从低到高分配;全局区/静态区:包括未初始化过和初始化过,全局区/静态区在内存中是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,比如 int a;未初始化的,int a=10;已初始化;常量区:常量字符串就是放在这里;代码区:存放App代码

2.什么是引用计数(retainCount)

引用计数是一个简单而有效的管理 OC 对象生命周期的方式,不管是 OC 还是 Swift 其内存管理方式(原理)都是基于引用计数的。引用计数也叫内存计数。

原理:

引用计数可以有效的管理对象的生命周期,当我们创建一个新对象的时候,他(该对象所在的内存块)的引用计数为1,当有一个新的指针指向这个对象时,我们将其引用计数加1,当某个指针不在指向这个指针时,我们将其应用计数减1,当对象的引用计数变为0时,说明这块内存不在被任何指针指向,这个时候系统就会将对象销毁,回收内存,从而达到管理内存。

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

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

如何知道对象已经没有被引用
  • 引用计数举例
引用计数举例
引用计数举例
  • 注意一些特殊的情况

NSString 引用计数问题比较复杂,具体参考链接:
https://blog.csdn.net/shisanshuno1/article/details/79810620
https://www.jianshu.com/p/990e5252e0fb

3.什么是指针和地址

指针:其实是一个内存地址,对于一个内存单元来说,单元的地址即为指针。指针是用于存放变量的地址,用于存放变量地址的变量称为“指针变量”。程序的执行过程中可以通过指针来找到要操作的数据和可执行的函数代码。

地址:表示内存空间的一个位置点,他是用来赋给指针的,地址本身是没有大小概念,指针指向变量的大小,取决于地址后面存放的变量类型。变量的地址:计算机的内存是以字节为单位的一片连续的存储空间,每一个字节都有一个编号,这个编号就是该内存的地址,并且这些编号是连续的。而一个变量实质上代表了“内存中的某个存储单元”,而这些存储单元的编号就是该变量的地址。

说明:
1.指针总共可以分为两种,函数指针和数据指针。
2.指针的使用一定要特别注意,不能越界,不要使用未初始化过的指针。

一级、二级、多级指针:https://blog.csdn.net/OMGMac/article/details/122152963

  • 指针和地址的区别

1.指针和地址最大的区别就是指针是有类型的,地址是没有类型的。我们可以通过绝对地址的方式找到函数和数据,但是地址是没有类型的,不能对地址进行算术操作,在涉及诸如数组等操作时就不能通过地址的自增和自减来访问数组的各个变量。但是通过对指针的引用,就可以通过对指针进行一系列的加加减减操作很方便的访问数组的各个元素。

2.指针是由地址和类型两部分构成的;指向数据的指针不仅记录该数据的在内存中的存放的地址,还记录该数据的类型,即在内存中占用几个字节,这是地址所不具有的。指向函数的指针不仅记录函数的入口地址,也记录该函数的类型,即函数的返回值类型和该函数的参数类型。

3.指针意味着已经有一个指针变量存在,他的值是一个地址,指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在。指针的值如果没有限制,通常是可以变化的也可以指向另外一个地址。

  • 指针变量的定义和指针变量的基类型

1.定义指针变量的一般形式如下:

类型名  *指针变量名1; *指针变量名2,...........;  
例如:int *p,*q;
注意:每个变量前的星号是一个说明符,用来说明该变量是指针变量。变量前的星号不可省略,若省略了星号说明符,就变成了将 p 和 q 定义为整形变量。

2.指针变量的基类型:

在这里主要说明一下为什么指针必须区分基类型。一个指针变量中存放的是一个存储单元的地址。这里“一个存储单元”中的“一”所代表的字节数是不同的:对 short int 类型整数而言,它代表2个字节;对 int 类型而言,它代表4个字节,这就是基类型的不同含义。在涉及指针的移动,也就是要对地址进行增减运算,这时指针移动的最小单位是一个储存单元,而不是1个字节。因此对于基类型不同的指针变量,其内容增1、减1所“跨越”的字节数不同,因此基类型不同的指针变量不能混合使用。

  • 常量指针和指针常量

https://juejin.cn/post/6972041530726940685

常量指针
指针常量

4.内存泄漏、野指针、空指针、僵尸对象

内存泄漏

我们知道 iOS 开发有 “ARC 机制” 帮忙管理内存,但在实际开发中如果处理不好堆空间上的内存还是会存在内存泄漏的问题。如果内存泄漏严重,最终会导致程序的崩溃。我们需要检查我们的 App 有没有内存泄漏,并且快速定位到内存泄漏的代码,比较常用的内存泄漏的排查方法有两种,都在 Xcode 中可以直接使用:第一种为静态分析方法(Analyze)、第二种为动态分析方法(Instrument 工具库里的 Leaks),一般推荐使用第二种。

相关链接:
https://juejin.cn/post/6844903681649803278
https://juejin.cn/post/7033233548249153550

  • 什么时候会发生内存泄漏

https://juejin.cn/post/6844904070344343565
https://www.jianshu.com/p/6c4849dcef55

  • 举例:栈 - 泄露
int number = 4;
int *a = malloc(8);
a = &number;
free(a);

给指针 a 分配了8个字节的地址,a 又指向了 number 的地址,最后 a 释放了。
这时候释放的是 number 的地址,而 number 是在栈区中,不能被手动释放,这时候就出现了栈的内存泄露。

野指针

野指针:野指针出现的原因是指针没有赋值或者指针指向的对象已经被释放掉了,野指针指向一块随机的垃圾内存,向他们发送消息会报 EXC_BAD_ACCESS 错误导致程序崩溃。野指针是不为 nil 但是指向已经被释放内存的指针,比如 __unsafe_unretain 或者 assign 的指针,对象释放后会出现野指针。

相关链接:
https://juejin.cn/post/6844903747538141191
https://juejin.cn/post/6930979515552235528

  • 什么情况下会产生野指针?
野指针需要满足两个条件
什么情况下会产生野指针
使用野指针可能出现的情况

空指针

空指针:空指针不同于野指针,它是一个没有指向任何东西的指针,空指针是有效指针,值为 nil、NULL、Nil 或0等,给空指针发送消息不会报错,只是不响应消息而已。应该给野指针及时赋予零值变成有效的空指针,避免内存报错。

僵尸对象

僵尸对象:1个已经被释放的对象就叫做僵尸对象。一个 OC 对象引用计数为0被释放后就变成僵尸对象了,僵尸对象的内存已经被系统回收,虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用,它的内存是随时可能被别的对象申请而占用的。该对象已经被释放,但是内存空间还没有被复写,当前对象还是可以访问,但是系统已标记为释放,随时有可能重新被分配进行复写,这个对象这时是不能访问的。

相关链接:https://juejin.cn/post/6844903801267159047

其他注意事项

1.避免循环引用,如果两个对象互相为对方的成员变量,那么这两个对象一定不能同时为 retain,否则,两个对象的 dealloc 函数形成死锁,两个对象都无法释放。

2.不要滥用 autorelease,如果一个对象的生命周期很清晰,那最好在结束使用后马上调用 release,过多的等待 autorelease 对象会给内存造成不必要的负担。

3.编码过程中建议将内存相关的函数,如 init、dealloc、viewdidload、viewdidunload 等函数放在最前,这样比较显眼,忘记处理的概率也会降低。

4.AutoReleasePool 对象在释放的过程中,在 iOS 下,release 和 drain 没有区别,但为了一致性还是推荐使用 drain。

提问

  • 如果一个对象释放前被加到了 NotificationCenter 中,不在 NotificationCenter 中 remove 这个对象可能会出现什么问题?

首先对于 NotificationCenter 的使用,我们都知道只要添加对象到消息中心进行通知注册,之后就一定要对其 remove 进行通知注销。将对象添加到消息中心后,消息中心只是保存该对象的地址,消息中心到时候会根据地址发送通知给该对象,但并没有取得该对象的强引用,对象的引用计数不会加1。如果对象释放后却没有从消息中心 remove 掉进行通知注销,也就是通知中心还保存着那个指针,而那个指针指的对象可能已经被释放销毁了,那个指针就成为一个野指针,当通知发生时会向这个野指针发送消息导致程序崩溃。

  • 提问:什么是安全释放?

释放掉不再使用的对象同时不会造成内存泄漏或指针悬挂问题称其为安全释放。

  • 提问:野指针是什么,iOS 开发中什么情况下会有野指针?

指针是不为 nil,但是指向已经被释放的内存的指针。 __unsafe_unretain 或者 assign 的指针,对象释放后会出现野指针。 一般情况下 OC 使用了 weak 指针,在对象销毁时指针会置 nil。

5.内存管理原则

一般是 “谁创建,谁释放” 的原则;OC 提供了两种种内存管理方式:MRC 手动引用计数器和 ARC 自动引用计数,苹果推荐 ARC 这个新技术来管理内存。

基本原则:

1.当你通过 new、alloc、copy 或 mutabelCopy 方法创建一个对象时,它的引用计数为1,当不再使用该对象时,应该向对象发送 release 或者 autorelease 消息释放对象。

2.当你通过其他方法获得一个对象时,如果对象引用计数为1且被设置为 autorelease,则不需要执行任何释放对象的操作;

3.如果你打算取得对象所有权,就需要保留对象并在操作完成之后释放,且必须保证 retain 和 release 的次数对等。

  • 注意:ARC 与 自动释放池区别?

区别就是 ARC 会自动为对象加上 retain,release,而自动释放池就是把 release 变成 autorelease。

1.在 MRC 中,调用 [obj autorelease] 来延迟内存的释放是一件简单自然的事,在 ARC 下我们甚至可以完全不知道 Autorelease 就能管理好内存,而在这背后 objc 和编译器都帮我们做了很多事情。

2.自动释放池是 OC 的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放,自动释放池本身销毁的时候,池子里面所有的对象都会做一次 release 操作。它可以延迟加入 autoreleasepool 中变量 release 的时机,创建的变量会在超出其作用域的时候 release,autorelease 本质上就是延迟调用 release。

3.在没有手动添加 autoreleasepool 的情况下,autoreleasepool 对象是在当前的 runloop 迭代结束时释放的,在收到内存警告时也会进行内存释放,而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 Push 和 Pop (每一个线程都有一个默认 autoreleasepool)。
https://www.jianshu.com/p/e69e303ba1b4

有三种情况是需要我们手动添加 autoreleasepool 的:
1.你编写的程序不是基于 UI 框架的,比如说命令行工具;
2.你编写的循环中创建了大量的临时对象;
3.你创建了一个辅助线程。

  • 补充:对象是什么时候被 release 的?

引用计数为0时被 release。autorelease 实际上只是把对 release 的调用延迟了,对于每一个 Autorelease,系统只是把该 Object 放入了当前的 Autorelease pool 中,当该 pool 被释放时该 pool 中的所有 Object 会被调用 Release。对于每一个 Runloop, 系统会隐式创建一个 Autorelease pool,这样所有的 release pool 会构成一个象 CallStack 一样的一个栈式结构,在每一个 Runloop 结束时,当前栈顶的 Autorelease pool 会被销毁,这样这个 pool 里的每个 Object(就是 autorelease 的对象)会被 release。那什么是一个 Runloop 呢? 一个 UI 事件,Timer call、 delegate call 都会是一个新的 Runloop。

疑问待解决?:就是比如创建应用运行的时候在main入口的时候就系统创建好了自动释放池,在iOS开发中现在就自动都是设置了ARC了。那么在开发中用对象等时候怎么使用这2者的或者什么时候用的哪个?

6.常用内存修饰词

在 iOS 开发中,我们最常用到的就是那些修饰属性的关键字。

内存管理有关的关键字:strong、retain、weak、assign、copy
线程安全有关的的关键字:nonatomic、atomic
访问权限有关的的关键字:readonly、readwrite(只读,可读写)
修饰变量的关键字:const、static、extern

相关链接:https://juejin.cn/post/6844903773131767816

内存管理有关的关键字

  • stronng:

用于修饰一些 OC 对象类型的数据,比如:NSNumber、NSString、NSArray、NSDate、NSDictionary、模型类等,它被一个强指针引用着,是一个强引用,指向对象内存地址,内存计数+1。在 ARC 环境下等同于 retain,这一点区别于 weak。它是一我们通常所说的指针拷贝(浅拷贝),内存地址保持不变,只是生成了一个新的指针,新指针和引用对象的指针指向同一个内存地址,没有生成新的对象,只是多了一个指向该对象的指针。

注意:由于使用的是一个内存地址,当该内存地址存储的内容发生变更的时候,会导致属性也跟着变更。

  • retain:

表示持有特性,setter 方法将传入参数先保留再赋值,传入参数的 retaincount 会+1。使用 retain 会对内存的引用计数进行操作以保证持有的对象不会被回收,retain:setter 方法对参数进行 release 旧值,再 retain 新值。

注意:retain 和 strong 区别:作用是一样的,只是写法上的区别。在非 ARC 机制时是用 retain 关键字修饰;在 ARC 机制后一般都用 strong 关键字来代替 retain 了。

  • release

与 retain 配对使用的方法是 release,因为 retain 是将内存的引用计数加一即对对象进行一次持有,release 是将内存的引用计数减一即结束对对象的持有。

  • copy

同样用于修饰 OC 对象类型的数据,同时在 MRC 时期用来修饰 block,因为 block 需要从栈区 copy 到堆区,在现在的 ARC 时代,系统自动给我们做了这个操作,所以现在使用 strong 或者 copy 来修饰 block 都是可以的。

注意:
1.copy 和 strong 相同点在于都是属于强引用,都会是属性的计数加1。
2.copy 和 strong 不同点在于,它所修饰的属性当引用一个属性值时,是内存拷贝(深拷贝),就是在引用时会生成一个新的内存地址和指针地址来,和引用对象完全没有相同点,因此它不会因为引用属性的变更而改变。
3.copy 在修饰 Mutable 可变类型会在内存里拷贝一份对象,两个指针指向不同的内存地址,copy 出来的新对象是不可变类型的。而修饰 NSString、NSArray 等普通类型充当 strong 使用,内存计数+1。具体的深浅拷贝情况解析看拷贝专讲。

举例:用 @property 声明的 NSString(或NSArray,NSDictionary)经常使用 copy 关键字,为什么?如果改用 strong 关键字,可能造成什么问题?
https://blog.csdn.net/weixin_33725515/article/details/88027657

  • retain、strong、copy 区别

https://blog.csdn.net/qiushisoftware/article/details/102647011
https://www.cnblogs.com/fengmin/p/5390073.html

  • weak:

同样经常用于修饰 OC 对象类型的数据,修饰的对象在释放后指针地址会自动被置为 nil,这是一种弱引用。更为具体的解析看 weak 专讲。

注意:在 ARC 环境下,为避免循环引用,往往会把 delegate 属性用 weak 修饰;在 MRC 下使用 assign 修饰。当一个对象不再有 strong 类型的指针指向它的时候,它就会被释放,即使还有 weak 型指针指向它,那么这些 weak 型指针也将被清除。

  • assign:

经常用于非指针变量,用于基础数据类型 (例如 NSInteger)和 C 数据类型(int、float、double、char 等),另外还有 id 类型。用于对基本数据类型进行复制操作,不更改引用计数。也可以用来修饰对象,但是被 assign 修饰的对象在释放后,指针的地址还是存在的,也就是说指针并没有被置为 nil,成为野指针。

注意:
1.之所以可以修饰基本数据类型,因为基本数据类型一般分配在栈上,栈的内存会由系统自动处理,不会造成野指针。
2.在 MRC 下常见的 id delegate 往往是用 assign 方式的属性而不是 retain 方式的属性,为了防止 delegation 两端产生不必要的循环引用。例如:对象 A 通过 retain 获取了对象 B 的所有权,这个对象 B 的 delegate 又是 A, 如果这个 delegate 是 retain 方式,两个都是强引用,互相持有,那基本上就没有机会释放这两个对象了。

  • weak 和 assign 的区别:

1.修饰的对象:weak 修饰 OC 对象类型的数据,assign 用来修饰是非指针变量。
2.引用计数:weak 和 assign 都不会增加引用计数。
3.释放:weak 修饰的对象释放后指针地址自动设置为 nil,assign 修饰的对象释放后指针地址依然存在,成为野指针。
4.修饰 delegate 在 MRC 使用 assign,在 ARC 使用 weak。

  • unsafe_unretained

unsafe_unretained 类型指针指向一块内存时,内存的引用计数也不会增加,这一点与 weak 一致。但是与 weak 类型不同的是,当其所指向的内存被销毁时(对象被销毁),unsafe_unretained 类型的指针并不会被赋值为 nil,也就是变成了一个野指针。对野指针指向的内存进行读写,程序就会 crash。

注意:当声明一个局部变量时,使用方式有点区别,要在关键词前面加双下划线__。

相关链接:https://www.jianshu.com/p/bd6aa1e62717

线程安全有关的的关键字

  • nonatomic

nonatomic 非原子操作:不加锁,线程执行快,但是多个线程访问同一个属性时结果无法预料。也就是多线程并发访问性能高,但是访问不安全。

  • atomic

atomic 原子操作:加锁,保证 getter 和 setter 存取方法的线程安全(仅对 setter 和 getter 方法加锁)。 因为线程加锁的原因,在别的线程来读写这个属性之前会先执行完当前的操作。

例如:线程 A 调用了某一属性的 setter 方法,在方法还未完成的情况下,线程 B 调用了该属性的 getter 方法,那么只有在执行完 A 线程的 setter 方法以后才执行 B 线程的 getter 操作。当几个线程同时调用同一属性的 setter 和 getter方法时会得到一个合法的值,但是 get 的值不可控(因为线程执行的顺序不确定)。

注意:atomic 只针对属性的 getter/setter 方法进行加锁,所以安全只是针对 getter/setter 方法来说,并不是整个线程安全,因为一个属性并不只有
getter/setter 方法。举例:如果一个线程正在 getter 或者 setter 时,有另外一个线程同时对该属性进行 release 操作,如果 release 先完成会造成 crash。

访问权限有关的的关键字

  • readonly

这个属性变量就是表明变量只有可读方法,也就是说你只能使用它的 get 方法。因为他只生成 getter 方法而不生成 setter方法(getter 方法没有 get 前缀)。

  • readwrite

这个属性是变量的默认属性,就是如果你 readwrite and readonly 都没有使用,那么你的变量就是 readwrite 属性,通过加入 readwrite 属性你的变量就会有 get 和 set 方法。它是可读可写特性,会生成不带额外参数的 getter 和 setter 方法(setter 方法只有一个参数)。

修饰变量的关键字

相关链接:
https://www.jianshu.com/p/2fd58ed2cf55
笔记17第11点:https://www.jianshu.com/p/b619c4286d26

  • const

常量修饰符,表示不可变。可以用来修饰右边的基本变量和指针变量,放在谁的前面修饰谁(基本数据变量 p,指针变量 *p)。

常用写法例如:

1.const 类型 * 变量名 a
可以改变指针的指向,不能改变指针指向的内容。 const 放前面约束参数,表示 *a 只读,只能修改地址 a,不能通过 a 修改访问的内存空间。

2.类型 * const 变量名
可以改变指针指向的内容,不能改变指针的指向。 const 放后面约束参数,表示 a 只读,不能修改 a 的地址,只能修改 a 访问的值,不能修改参数的地址。

  • 常量(const)和宏定义(define)的区别:

使用宏和常量所占用的内存差别不大,宏定义的是常量,常量都放在常量区,只会生成一份内存。

1.编译时刻:宏是预编译(编译之前处理),const 是编译阶段。导致使用宏定义过多的话,随着工程越来越大,编译速度会越来越慢。
2.宏不做检查,不会报编译错误,只是替换,const 会编译检查,会报编译错误。
3.宏能定义一些函数和方法, const 不能。

  • static

定义所修饰的对象只能在当前文件访问,不能通过 extern 来引用。默认情况下的全局变量作用域是整个程序(可以通过 extern 来引用),被 static 修饰后仅限于当前文件来引用,其他文件不能通过 extern 来引用。

修饰局部变量:
1.有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用的时候该变量已经有值。这时就应该指定该局部变量为静态变量,用关键字 static 进行声明。
2.延长局部变量的生命周期(没有改变变量的作用域,只在当前作用域有用),程序结束才会销毁。
3.局部变量只会生成一份内存,只会初始化一次。把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在。

注意:当在对象 A 里这么写 static int i = 10; 当 A 销毁掉之后,这个 i 还存在。当我再次 alloc init 一个 A 的对象之后,在新对象里依然可以拿到 i = 90,除非杀死程序再次进入才能得到 i = 0。

修饰全局变量:
1.只能在本文件中访问,修改全局变量的作用域,生命周期不会改。
2.避免重复定义全局变量(单例模式)。

  • extern

只是用来获取全局变量(包括全局静态变量)的值,不能用于定义变量。先在当前文件查找有没有全局变量,没有找到才会去其他文件查找(优先级)。

  • const、static、extern 组合使用
static与const联合使用
extern与const联合使用

@property、@synthesize、@dynamic

相关链接:
https://www.jianshu.com/p/490424151e31
https://www.ngui.cc/el/1176329.html?action=onClick

  • @Property

1.属性(property)是 Objective-C 的一项特性,用于封装对象中的数据。这一特性可以令编译器自动编写与属性相关的存取方法,并且保存为各种实例变量。

2.属性的本质是实例变量与存取方法的结合,@property = ivar + getter + setter

3.@property 会使编译器自动编写访问这些属性所需的方法,此过程在编译期完成,称为自动合成 (autosynthesis)。与此相关的还有两个关键词:@dynamic 和 @synthesize。

  • @dynamic

告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。即使编译器发现没有定义存取方法也不会报错,运行期会导致崩溃。

  • @synthesize

在类的实现文件里可以通过 @synthesize 指定实例变量的名称。

  • 注意

在 Xcode 较早版本之前,@property 配合 @synthesize 使用,@property 负责声明属性,@synthesize 负责让编译器生成 带下划线的实例变量并且自动生成 setter、getter 方法。Xcode 新版本之后 @property 得到增强,直接一并替代了 @synthesize 的工作。

其他

7.alloc、init、new、dealloc 区别

alloc

alloc:开辟一块内存给对象,让它不释放并且把地址返回给指针。

通过查看 objc 源码发现 alloc 方法主要做了三件事情:
1.计算申请内存空间大小
2.开辟内存空间
3.把类的信息关联到 isa 上

alloc 实现
alloc 实现

init

init:对这块内存进行初始化。通过查看 objc 源码发现 init 方法什么事情都没做,而是直接 return (id)self; 。没什么复杂的,苹果这样写原因:init 只是一个构造方法,为我们自定义类的时候重写提供便利。自定义类往往会有自己的构造方法即重写 init,如 - (instancetype)initWithName: (NSString *)name;

new

通过查看源码 new 调用的是 [callAlloc(self, false) init], 相当于调用了 alloc init 两个方法。区别就在于 new 不会调用自定义的构造方法,所以日常开发中不建义使用 new。

dealloc

dealloc:对象的销毁,对象要释放必须执行根类 NSObject 中 -(void)dealloc()
方法,因为该方法最终会去执行 C++ 的 objc_object::rootDealloc() 方法。对象的销毁最终就是在 rootDealloc() 中销毁。

注意:

1.MRC 要调用 [super dealloc]; ARC 不能调用 [super dealloc];

2.MRC 要调用:在 MRC 环境下系统不会自动释放对象。由于对象的释放必须调用 c++ 的 rootDealloc() 方法。因此一旦我们重写了对象的 dealloc 方法,如果不调用 [super dealloc]; 那么就不会调用到根类 NSObject 中 -(void)dealloc() 方法。对象也就释放不掉,只是单单执行了该类 -(void)dealloc() 方法并没有释放对象。所以 MRC 一旦重写了 -(void)dealloc() 方法一定要调用 [super dealloc];,这样才能释放该实例对象。

3.ARC 不能调用:ARC 会自动帮我们在代码的适当位置插入 [obj release]; 我们什么也不做,ARC 就调用了一次 C++ 的 objc_object::rootDealloc() 方法。此时如果我们再次调用了 [super dealloc]; 方法,那么最终会调用根类 NSObject 的 release() 方法,就会执行到 C++ 的 objc_object::rootDealloc() 方法,由于 ARC 已经释放该对象了,这时再次 free(this) 释放该对象会发生错误。所以 ARC 环境下不能调用 [super dealloc];,不然会导致该实例对象重复释放。

相关链接:
https://blog.csdn.net/pk_sir/article/details/107384255

8.强引用、弱引用、循环引用

默认情况下,一个指针都会使用__strong 属性,表明这是一个强引用。当所有强引用都去除时,对象才能被释放。但有时候我们可能要禁止这种行为:一些集合类不应该增加其元素的引用,可能对导致对象无法释放,可能会出现循环引用,导致无法释放,这种情况下我们要使用弱引用 __weak。weak 是弱引用,计数器不会加1,并在引用对象被释放的时候自动设置为 nil。ARC 提供四种修饰符分别是:__strong、__weak、__autoreleasing、__unsafe_unretained。

相关链接:http://t.zoukankan.com/geek6-p-3947985.html

  • 补充:weak 属性修饰词与 __weak 什么关系,一个东西?

weak 属性修饰词:修饰属性对象,属于弱引用,不使用的时候直接释放内存,不会造成野指针。__weak:用来解决循环引用的闭环,达到销毁对象的效果。底层应该都差不多,只是使用场景的差别不同。

与所有权有关系的属性和关键字之间的对应关系

强引用(__strong)

强引用:持有所指向对象的所有权,无修饰符情况下程序默认的变量修饰符就是 __strong。如需强制释放可置 nil。在 ARC 中修饰符是 __strong,比如 __strong NSObject *obj;

注意:__strong 与变量:在 ARC 模式下,id 类型和 OC 对象的所有权修饰符默认是 __strong。当一个变量通过 __strong 修饰符来修饰,当该变量超出其所在作用域后,该变量就会被废弃,同时值给该变量的对象也会被释放。__strong 修饰后,对象的引用计数会增加,在作用域外不会销毁。

弱引用(__weak)

弱引用:不持有所指向对象的所有权,引用指向的对象内存被回收之后,引用本身会置 nil 避免野指针。在 ARC 中修饰符是 __weak,比如 __weak NSObject *obj; 比如避免循环引用的弱引用声明:__weak __typeof(self) weakSelf = self;

注意:__weak 修饰后,对象引用计数不会增加,在作用域外会自动置为 nil。

  • 强引用与弱引用区别

1.两者区别简单讲的话就是:强引用持有对象,而弱引用不持有对象。
2.什么时候用 strong 和 weak,平时一般都是用 strong,也就是默认不添加,在会照成循环引用时才使用 weak。

循环引用

1.当两个不同的对象各有一个强引用指向对方,那么循环引用就产生了,每个对象的引用计数都会+1,无法得到内存的释放。只有在堆中的相互引用无法回收,有可能造成循环引用,简单来说就是调用了 new,alloc 等关键字才有可能出现循环引用。
2.循环引用的实质是多个对象相互之间有强引用,不能释放导致无法回收,解决方法一般是将 strong 引用改为 weak 引用。

  • 代理(delegate)循环引用问题

delegate 是 iOS 开发中比较常遇到的循环引用,一般在声明 delegate 的时候都要使用弱引用 weak 或者 assign,当然怎么选择使用 assign 还是 weak,MRC 的话只能用 assign,在 ARC 的情况下最好使用 weak,因为 weak 修饰的变量在释放后自动指向 nil,防止野指针存在。
delegate 属性的声明一般是 weak:@property (nonatomic, weak) id <TestDelegate> delegate;

  • block 循环引用问题

1.由于 block 会对 block 中的对象进行持有操作,就相当于持有了其中的对象,如果此时 block 中的对象又持有了该 block 则会造成循环引用,解决方案就是使用 weak 进行弱引用。比如 block 在 copy 时都会对 block 内部用到的对象进行强引用造成循环引用。
2.并不是所有的 block 都会造成循环引用,只有被强引用了的 block 才会产生循环引用,具体看情况。比如 dispatch_async(dispatch-get_main_queue(),^{}),[UIView animateWithDuration:1 animations:^{}] 这些系统方法等,或者 block 并不是其属性而是临时变量即栈 block,这些都不会产生循环引用。

相关链接:https://juejin.cn/post/6998919742010425357

1.block 循环引用举例:

self.myBlock = ^{
     [self doSomething];//这个 self 就是强引用
};

由于 block 会对 block 中的对象进行持有操作,就相当于持有了其中的对象,如果此时 block 中的对象又持有了该 block,则会造成循环引用。
解决方案就是使用 __weak 修饰 self 即可:

__weak typeof(self) weakSelf = self;
self.myBlock = ^{
     [weakSelf doSomething];
};

2.使用 block 不会造成循环引用的说明:

使用 block 不会造成循环引用的说明

3.__block 的作用

__block 的根本作用就是把 block 的外部变量的地址从栈区放到堆区。__block 只要观察到某一个成员变量被 block 所持有,就会把该变量的内存地址从栈区放到堆区。因此在堆区的该成员变量就会变成有用户自己分配和释放,不会被系统管理造成丢失。

相互链接:https://www.jianshu.com/p/3d3878649b4d

  • Timer 定时器循环引用问题

在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,那么该对象和定时器就相互循环引用了。解决办法为:可以使用手动断开循环引用,如果是不重复定时器,在回调方法里将定时器 invalidate 并置为 nil 即可;如果是重复定时器,在合适的位置将其 invalidate 并置为 nil 即可。

相关链接:
https://juejin.cn/post/6992501463058481188
https://juejin.cn/post/6844903968250789896

__unsafe_unretained

__unsafe_unretained 修饰后引用计数不会增加,在作用域外不会置空,会造成野指针闪退。

9.weak详解

概念

weak 是弱引用,用 weak 来修饰、描述所引用对象的计数器并不会增加,而且 weak 会在引用对象被释放的时候自动置为 nil,这也就避免了野指针访问坏内存而引起奔溃的情况,另外 weak 也可以解决循环引用

拓展:为什么修饰代理使用 weak 而不是用 assign?
assign 可用来修饰基本数据类型,也可修饰 OC 的对象,但如果用 assign 修饰对象类型指向的是一个强指针,当指向的这个指针释放之后,它仍指向这块内存,必须要手动给置为 nil,否则会产生野指针。如果还通过此指针操作那块内存,会导致EXC_BAD_ACCESS 错误,因为调用了已经被释放的内存空间。而 weak 只能用来修饰 OC 对象,而且相比 assign 比较安全,如果指向的对象消失了,那么它会自动置为 nil,不会导致野指针。

相关链接:
https://www.jianshu.com/p/fec68e84cee9
https://juejin.cn/post/6993634762896195615

weak实现原理的概括

weak 实现原理的概括

当 weak 引用指向的对象被释放时,又是如何去处理 weak 指针的呢?

当释放对象时,其基本流程如下:
1.调用 objc_release
2.因为对象的引用计数为0,所以执行 dealloc
3.在 dealloc 中,调用了 _objc_rootDealloc 函数
4.在 _objc_rootDealloc 中,调用了 object_dispose 函数
5.调用 objc_destructInstance
6.最后调用 objc_clear_deallocating
调用的 objc_clear_deallocating 函数中,调用了 clearDeallocating,它最终是使用了迭代器来取 weak 表的 value,然后调用 weak_clear_no_lock,然后查找对应的 value,将该 weak 指针置空。

IBOutlet 连出来的视图属性为什么可以被设置成 weak?

在 storyboard 中添加一个控件引用关系是这样的(以 UIbutton 为例): UIviewController -> UIview -> UIbutton。此时 UIviewController 强引用着 UIview,UIview 强引用着 UIbutton,IBoutlet 连线到控制器的 .m 或者 .h 中作为视图的属性时用 weak 修饰就可以了, (觉得用 strong 修饰也可以但是没有必要)。添加到子控件也是强引用: UIbutton 就是添加到了 UIviewController 的 view 上。

总结

1.为了管理所有的引用计数和 weak 指针,苹果创建了一个全局的 SideTables,它是一个全局的 hash 表(即 weak 的原理在于底层维护了一张 weak_table_t 结构的 hash 表),用于存储指向某个对象的所有 weak 指针,key 是所指向对象的地址,value 是 weak 指针的地址数组,里面存放的都是 SideTable 结构体。
2.weak 关键字的作用是弱引用,计数器不会加1,并在引用对象被释放的时候自动设置为 nil。
3.对象引用计数相关的操作是原子性的,如果多个线程同事操作一个对象的引用计数会造成数据错乱,同时在内存中的对象数据量大,不能读整个 Hash 加锁,所以苹果采用了分离锁。
4.对象释放时,调用 clearDeallocating 函数根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

10.深浅拷贝理解

1.简单理解就是:浅拷贝是拷贝了指向对象的指针, 深拷贝不但拷贝了对象的指针还在系统中再分配一块内存,存放拷贝对象的内容。

2.浅拷贝:拷贝对象本身,返回一个对象,指向相同的内存地址。 深拷贝:拷贝对象本身,返回一个对象,指向不同的内存地址。

3.浅拷贝与深拷贝的本质区别:在于是否在堆内存中开辟新的内存空间。浅拷贝并不是拷贝对象本身,而是对指向对象的指针进行拷贝,但还是指向同一块堆内存中指针指向的对象。深拷贝直接拷贝对象到内存中的一块区域,然后把新对象的指针指向这块内存。

浅拷贝:可以看出浅拷贝中栈内存中指针对象的地址改变了,但还是指向相同的一块堆内存地址
深拷贝:可以看出深拷贝是直接拷贝对象到内存中的一块区域(分配了一块新的内存空间),然后把新对象的指针指向这块内存,原对象和被赋值对象互不影响
  • 关于 Copy 的使用

1.在 oc 中 copy 是利用一个源对象产生一个副本对象,本质就是修改源对象的属性和行为,不会影响副本对象,同样修改副本对象的属性和行为,不会影响源对象。

2.如何使用 copy:一个对象可以调用 copy 或 mutableCopy 方法来创建一个副本对象;copy 创建的是不可变副本(NSString、NSArray、NSDictionary…);mutableCopy 创建的是可变副本(如NSMutableString、NSMutableArray、NSMutableDictionary…)。

3.copy使用的前提:必须按 NSCopying 协议实现 copyWithZone: 方法

@protocol NSCopying
- (id)copyWithZone:(NSZone *)zone;
@end

4.mutableCopy的使用前提:必须按 NSMutableCopying 协议实现 mutableCopyWithZone: 方法

@protocol NSMutableCopying
- (id)mutableCopyWithZone:(NSZone *)zone;
@end
  • 举例说明深浅拷贝

先来形象解释一波:

解释1:

浅拷贝即如果拷贝一个对象,而不拷贝它的子对象;
深拷贝:拷贝一个对象时连同子对象一起拷贝

解释2:

浅拷贝:向retain一样,创建一个指针指向了同一块内存空间(即同一个对象,不产生新对像),使该内存空间引用计数加1
深拷贝:向copy一样,复制一个对象,产生了一个新的对象;新对象的引用计数为1,旧对象的引用计数不变

补充 - 深拷贝用归档和解归档可以实现:

例子:人---关联一个车---车再关联一个引擎
应用场景:用初始化alloc init 创建对象,速度慢,内存...;所以考虑用复制对象方式来创建

1.浅复制(浅拷贝,指针拷贝,shallow copy)

  • 源对象和副本对象是同一个对象
  • 源对象(副本对象)引用计数器+1,相当于做一次retain操作
  • 本质:没有产生新的对象
NSString *srcStr = @"1gcode";
NSString *copyStr = [srcStr copy];
NSLog(@"srcStr = %p, copyStr = %p", srcStr, copyStr);

2.深复制(深拷贝,内容拷贝,deep copy)

  • 源对象和副本对象是不同的两个对象
  • 源对象引用计数器不变,副本对象计数器为1(因为是新产生的)
  • 本质:产生了新的对象
NSString *srcStr = @"1gcode";
NSMutableString *copyStr = [srcStr mutableCopy];
NSLog(@"src = %p, copy = %p", srcStr, copyStr);
NSLog(@"src = %@, copy = %@", srcStr, copyStr);
[copyStr appendString:@" cool"];
NSLog(@"src = %@, copy = %@", srcStr, copyStr);
NSMutableString *srcStr = [NSMutableString stringWithFormat:@"1gcode"];
NSString *copyStr = [srcStr copy];
[srcStr appendString:@".com"];
NSLog(@"src = %p, copy = %p", srcStr, copyStr);
NSLog(@"src = %@, copy = %@", srcStr, copyStr);
NSMutableString *srcStr = [NSMutableString stringWithFormat:@"1gcode"];
NSMutableString *copyStr = [srcStr mutableCopy];
[srcStr appendString:@".com"];
[copyStr appendString:@" good"];
NSLog(@"src = %p, copy = %p", srcStr, copyStr);
NSLog(@"src = %@, copy = %@", srcStr, copyStr);
  • copy与内存管理

1.如果是浅拷贝,不会生成新的对象,但是系统就会对原来的对象进行retain,所以我们要对原来的对象进行一次

2.如果是深拷贝,会生成新的对象,系统不会对原来的对象进行retain,但是因为生成了新的对象,所以我们要对新的对象进行release

对于浅拷贝:原对象引用计数器+1,必须释放原对象

NSString *srcStr = [[NSString alloc] initWithFormat:@"www.1gcode.com"];
NSLog(@"srcStr = %lu", [srcStr retainCount]);
NSString *copyStr = [srcStr copy];
NSLog(@"copyStr = %lu", [copyStr retainCount]);
[copyStr release]; // 必须做一次release

对于深拷贝(即深复制):新对象引用计数器+1,必须释放新对象

NSString *srcStr = [[NSString alloc] initWithFormat:@"www.1gcode.com"];
NSLog(@"srcStr = %lu", [srcStr retainCount]);
NSString *copyStr = [srcStr mutableCopy];
NSLog(@"copyStr = %lu", [copyStr retainCount]);
[copyStr release]; // 必须做一次release
  • 小结

1.浅拷贝是指针的拷贝,深拷贝是地址的拷贝

2.就是有可变的就是深拷贝;比如[NSString mutableCopy]这种就是深拷贝,[NSMutableString copy]也是深拷贝

3.可变就是要保留原有的对象,操作copy出来的对象,这两个对象的指针指向两个地址;这样做目的是保证原来的对象不被修改(如果你原来的对象都改了,你原来的对象本来要干的东西就干不了了)

4.浅拷贝不会拷贝地址只会拷贝指针,如果你要修改对象的内容,那你以前的那个对象也被修改了

5.浅复制好比你和你的影子,你完蛋,你的影子也完蛋;深复制好比你和你的克隆人,你完蛋,你的克隆人还活着

  • 如何判断浅拷贝、深拷贝?

深浅拷贝取决于拷贝后的对象的是不是和被拷贝对象的地址相同;
1.如果不同,则产生了新的对象,则执行的是深拷贝。
2.如果相同,则只是指针拷贝,相当于 retain 一次原对象,执行的是浅拷贝。

如何判断浅拷贝、深拷贝?深拷贝和浅拷贝的判断要注意两点:源对象类型是否是可变的、执行的拷贝是 copy 还是 mutableCopy
区分深拷贝与浅拷贝
  • 如果 NSMutableArray 用 copy 修饰可以不可以?

当 copy 修饰可变类型集合(例如:NSMutableArray)时,赋值后,会导致可变类型属性变为不可变类型,然后在调用可变类型方法时,会产生异常错误。产生异常的原因是 copy 属性在运行时赋值时调用了 -copyWithZone: 赋值,将可变类型转换为不可变类型。

补充说明:
比如这个写法:@property (nonatomic, copy) NSMutableArray *array; 如果使用这个 array 会闪退,因为 copy 出来的是一个不可变的数组。比如调用的时候是添加元素,那么错误会是数组找不到添加元素的方法,所以会闪退。

  • block 变量定义时为什么用 copy?block 是放在哪里的?

默认情况下,block 是存档在栈中,可能被随时回收,通过 copy 操作可以使其在堆中保留一份, 相当于一直强引用着,因此如果 block 中用到 self 时需要将其弱化,通过 __weak 或者 __unsafe_unretained。

  • copy 与 @property

1.@property使用copy可以防止外界修改内部的数据
2.可以使用copy保存block, 这样可以避免在block中使用的外界对象的时候, 外界的对象已经释放出现的野指针错误
3.注意: copy block可能会引发循环引用,如果对象中的block又用到了对象自己, 那么为了避免内存泄露, 应该将对象修饰为__block

  • 补充:block 内存相关
  • __block 什么时候用?
    在 block 里面修改局部变量的值都要用 __block 修饰

  • 在 block 里面,对数组执行添加操作,这个数组需要声明成 __block 吗?
    不需要声明成 __block,因为 testArr 数组的指针并没有变。往数组里面添加对象,指针是没变的,只是指针指向的内存里面的内容变了。

  • 在block 里面对 NSInteger 进行修改,这个 NSInteger 是否需要声明成 __blcok?
    NSInteger 的值发生改变,则要求添加 __block 修饰。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容