《Objective-C基础教程》读书笔记6—内存管理

内存管理
内存管理是程序设计中常见的资源管理的一部分。
虽然说当程序运行结束时,操作系统将收回其占用的资源,但是只要程序还在运行,它就会一直占用资源。如果不进行清理,某些资源最终将被耗尽,程序有可能会崩溃。而且随着操作系统的发展,程序何时终止运行会变得更加难以捉摸。
每个程序都会消耗内存。
使用脚本语言的程序员则不需要考虑此类问题:这些语言的内存管理是自动进行的。
对于内存管理,我们需要注意两点:
1.我们必须确保在需要的时候分配内存,在程序运行结束时释放占用的内存。如果我们只分配而不释放内存,则会发生内存泄漏(leak memory): 程序的内存占用量不断增加,最终会被耗尽并导致程序崩溃。
2.不要使用任何刚释放的内存,否则可能误用陈旧的数据,从而引发各种各样的错误,而且如果该内存已经加载了其他数据,将会破坏这些新数据。
1.1对象生命周期
对象的生命周期包括诞生(通过alloc或new方法实现)、生存(接收消息并执行操作)、交友(通过复合以及向方法传递参数)以及最终死去(被释放掉)。当生命周期结束时,它们的原材料(内存)将被回收以供新的对象使用。
1.1.1引用计数
Cocoa采用了一种引用计数(reference counting)的技术,有时也叫做保留技术(retain counting)。每个对象都有一个与之相关联的整数,被称作它的引用计数器或保留技术器。
当使用alloc、new方法或者通过copy消息(接收到消息的对象会创建一个自身的副本)创建一个对象时,对象的保留计数器值被设置为1。要增加对象的保留计数器的值,可以给对象发送一条retain消息。要减少的话,可以给对象发送一条release消息。
当一个对象因其保留计数器归0而即将被销毁时,Objective-C会自动向对象发送一条dealloc消息。你可以在自己的对象中重写dealloc方法,这样就能释放掉已经分配的全部相关资源。一定不要直接调用dealloc方法,Objective-C会在需要销毁对象时自动调用它。
如果对一个对象使用了alloc、new或copy操作,释放该对象(向该对象发送release消息)就能销毁它并能收回它所占用的内存。
1.1.2对象所有权(object ownership)
当我们说某个实体“拥有一个对象”时,就意味着该实体要负责确保对其所拥有的对象进行清理。如果一个对象内有指向其他对象的实例变量,则称该对象拥有这些对象。
当多个实体拥有某个特定对象时,对象的所有权关系就更加复杂了,这也是保留计数器的值大于1的原因。也许也正是内存管理这一模块很复杂的原因。
对象所有权最为本质的问题: 就是谁负责确保对象不再被使用时能够收到release消息。
1.1.3 访问方法中的保留和释放
最合理的setter方法
-(void)setEngine:(Engine *)newEngine{
[newEngine retain];
[engine release];
engine = newEngine;
}
上述写法的思想:先保留新对象,再释放旧对象。这种写法避免了两个问题的发生:
第一个问题:旧对象的内存泄漏问题
第二个问题:新对象与旧对象如果是同一个对象,引发的野指针问题。
1.1.4自动释放
比较复杂的问题:在某些情况下弄清楚什么时候不再使用一个对象并不容易。
最常见的是description方法。调用description方法的代码将返回的字符串存储在某个变量中,并在使用完毕后将其释放。思想:使用完毕后再释放。
1.1.5所有对象放入池中
自动释放池(autorelease pool): 是一个用来存放对象的池子(集合),并且能够自动释放。
NSObject类提供了一个叫做autorelease的方法:
-(id)autorelease;
该方法预先设定了一条会在未来某个时间发送的release消息,其返回值是接收这条消息的对象。这一特性与retain消息采用了相同的技术,使嵌套调用更加容易。当给一个对象发送autorelease消息时,实际上将该对象添加到了自动释放池中。当自动释放池被销毁时,会向该池中的所有对象发送release消息。
1.1.6自动释放池的销毁时间
有两种方法可以创建一个自动释放池。
1.通过@autoreleasepool关键字
2.通过NSAutoreleasePool对象
在我们一直使用的Foundation库工具集中,创建和销毁自动释放池已经由@autorelease关键字完成。当你使用@autorelease{}时,所有在花括号里的代码都会被放入这个新池子里。如果你的程序运算是内存密集型的,你可以使用这种自动释放池。有一点需要记住,任何在花括号里定义的变量在括号外就无法使用了。类似于C语言中的有效范围。
第二种更加明确的方法就是使用NSAutoreleasePool对象。如果你使用了这个方法,创建和释放NSAutoreleasePool对象之间的代码就会使用这个新的池子。创建了一个自动释放池后,该池就会自动成为活动的池子。释放该池后,其保留计数器的值归0,然后该池被销毁。在销毁的过程中,该池将释放其所包含的所有对象。
推荐使用关键字方法。它比对象方法更快,因为OC语言创建和释放内存的能力远在我们之上。
使用Appkit时,Cocoa定期自动地为你创建和销毁自动释放池。通常是在程序处理完当前事件(如鼠标单击或键盘按下)以后执行这些操作。你可以使用任意数目的自动释放池对象,当不再使用它们时,自动释放池将自动为你清理这些对象。
你可能在Xcode的自动生成代码中见过另一种销毁自动释放池中对象的方式:-drain方法。该方法只是清空自动释放池而不会销毁它,而且只适用于Mac OSX 10.4(Tiger)以后的操作系统版本。在自己编写(而不是由Xcode生成)的代码中,我们使用-release方法,因为该方法可以支持更古老的OSX版本。
1.1.7 自动释放池的工作流程
当对象接收到一条autorelease消息时,其保留计数器的值并不会发生改变。该对象只是被放入了NSAutoreleasePool当中。(NSAutoreleasePool是一个普通对象,与其他对象一样要遵从相同的内存管理规则。) 当自动释放池被销毁时,会向池中所有的对象发送一条release消息,所有被自动释放的对象都将其保留计数器的值减1。如果保留计数器的值归0了,则对象被销毁。深入理解就是:如果对象的引用计数不为0,则该对象依然存在,所以才会造成内存泄漏。(该释放的对象未被释放) 使用@autoreleasepool也能达到同样的目的,不过它并不需要分配或销毁自动释放池对象。
在使用AppKit或UIKit的时候,自动释放池会在明确的时间创建或释放,比如在处理当前用户事件的时候。除此以外,你要创建自己的自动释放池。Foundation库工具集的模板中包含了这些代码。
1.2Cocoa的内存管理规则
我们已经学习了各种方法:retain、release和autorelease。Cocoa有许多内存管理约定,它们都是一些很简单的规则,可应用于整个工具集内。
这些规则如下:
1.当你使用new、alloc或copy方法创建一个对象时,该对象的保留计数器的值为1。当不再使用该对象时,你应该向该对象发送一条release或autorelease消息。这样,该对象将在其使用寿命结束时被销毁。
2.当你通过其他方法获得一个对象时,假设该对象的保留计数器的值为1,而且已经被设置为自动释放,那么你不需要执行任何操作来确保该对象得到清理。如果你打算在一段时间内拥有该对象,则需要保留它并确保在操作完成时释放它。
3.你保留了某个对象,就需要(最终)释放或自动释放该对象。必须保持retain方法和release方法的使用次数相等。
如果你使用了new、alloc或copy方法获得一个对象,就释放或自动释放该对象。只要记住这条规则,就不用担心内存释放的问题了。
无论什么时候拥有一个对象,有两件事必须弄清楚:怎样获得该对象的?打算拥有多长时间?
1.2.1临时对象
第一个例子:例如可变数组,如果你是用new、alloc或copy方法获得的这个对象,就需要安排好该对象的内存释放,通常使用release消息来实现。如果你使用arrayWithCapacity:方法,则不需要关心如何销毁该对象。arrayWithCapacity:方法与alloc、new、copy这三个方法不同,因此可以假设该对象被返回时保留计数器的值为1且已经被设置为自动释放。当自动释放池被销毁时,向array对象发送release消息,该对象的保留计数器的值归0,其占用的内存被回收。
第二个例子:blueColor方法也不属于alloc、new、copy这三个方法,因此可以假设该对象的保留计数器的值为1并且已经被设置为自动释放。
全局单例(singleton)对象——每个需要访问它的程序都可以共享的单一对象,这个对象永远不会被销毁。
1.2.2拥有对象
如果你使用了new、alloc或copy方法获得了一个对象,则不需要执行任何其他操作。该对象的保留计数器的值为1,因此它将一直存在着,你只需要确保在拥有该对象的dealloc方法中释放它即可。
如果你使用除alloc、new或copy以外的方法获得了一个对象,需要记得保留该对象。
如果你编写的是GUI程序,要考虑到事件循环。你需要保留自动释放的对象,以便这些对象在当前的事件循环结束以后仍能继续存在。
什么是事件循环呢?一个典型的图形应用程序往往花费了许多时间等待用户操作。在控制程序运行的人作出决定(比如点击鼠标或者按下某个键)以前,程序将一直处于空闲状态。当发生这样的事件时,程序将被唤醒并开始工作,执行必要的操作以响应这一事件。在处理完这一事件后,程序返回到休眠状态并等待下一个事件发生。事件循环即:在事件发生时,程序被唤醒;在执行完某个事件所对应的相应的操作后,就处于休眠空闲状态,等待下一个事件的发生。为了降低程序的内存空间占用,Cocoa会在程序开始处理事件之前创建一个自动释放池,并在事件处理结束后销毁。这样可以尽量减少累积的临时对象的数量。
自动释放池被清理的时间是完全确定的:要么是在代码中你自己手动销毁,要么是使用AppKit时在事件循环结束时销毁。你不必担心程序会随机地销毁自动释放池,也不必保留使用的每一个对象,因为在调用函数的过程中自动释放池不会被销毁。(在循环执行过程中自动释放池对象不会被销毁)
解决累积大量临时对象而导致程序占用内存增长问题的方法:在循环中手动创建自动释放池,并在特定时间销毁。
自动释放池的分配和销毁操作代价很小,因此你甚至可以在循环的每次迭代中创建一个新的自动释放池。
自动释放池以栈的形式实现:当你创建了一个新的自动释放池时,它就被添加到栈顶。接收autorelease消息的对象被放入最顶端的自动释放池中。如果将一个对象放入一个自动释放池中,然后创建一个新的自动释放池,再销毁该新建的自动释放池,则这个自动释放池对象仍将存在,因为容纳该对象的自动释放池仍然存在。
1.2.3垃圾回收
Objective-C 2.0引入了自动内存管理机制,也称垃圾回收。Java或者Python等语言的内存管理机制就是垃圾回收。对于已经创建和使用的对象,当你忘记清理时,系统会自动识别哪些对象仍在使用,哪些对象可以回收。
启用垃圾回收功能非常简单,但它是一个可选择是否启用的功能。只需要转到项目信息窗口的Building Settings选项卡,并选择Required[-fobjc-gc-only]选项即可。-fobjc-gc选项是针对既支持垃圾回收又支持对象的保留和释放的代码,例如在两种环境下都能使用的库代码。
启用垃圾回收以后,平常的内存管理命令都变成了空操作指令,说得直白点就是它们不执行任何操作。
Objective-C的垃圾回收是一代新型的垃圾回收器。与那些面世已久的对象相比,新创建的对象更可能被当成垃圾。垃圾回收器定期检查变量和对象并且跟踪它们之间的指针,当发现没有任何变量指向某个对象时,就将该对象视为应该丢弃的垃圾。最糟糕的事情莫过于让指针指向一个不再使用的对象。因此如果你在一个实例变量中指向某个对象,一定要将该实例变量赋值为nil, 以取消对该对象的引用并告知垃圾回收器该对象可以清理了。
与自动释放池一样,垃圾回收器也是在事件循环结束时触发的。当然,如果编写的不是GUI程序,你也可以自己触发垃圾回收器。
垃圾回收功能只支持OS X应用开发,无法用在iOS应用程序上。实际上在iOS开发中,苹果公司建议你不要在自己的代码中使用autorelease方法,也不要使用会返回自动释放对象的一些便利的方法。(比如NSString, stringWith开头的方法都是便利方法)
1.2.4自动引用计数
垃圾回收机制会对移动设备的可用性产生非常不利的影响,因为移动设备比电脑更私人化,资源更少。
苹果公司的解决方案被称为自动引用计数,即ARC。ARC会追踪你的对象并决定哪一个仍会使用哪一个不会再用到。如果你启用了ARC, 只管像平常那样按需分配并使用对象,编译器会帮你插入retain和release语句,无需你自己动手。垃圾回收器在运行时工作,通过返回的代码来定期检查对象。与此相反,ARC是在编译时进行工作的。它在代码中插入了合适的retain和release语句,就好像是你自己动手写好了所有的内存管理代码。不过,是编译器替你完成了内存管理的工作。程序在运行的时候,retain和release会像往常一样发挥作用。系统不会知道也不会关心这些命令是你写的还是编译器写的。
ARC是一个可选的功能,也就是说你必须明确地启用或禁用它。
以下是编写ARC代码所需的条件:
Xcode4.2以上的版本;
Apple LLVM 3.0以上版本的编译器;
OS X10.7以上版本的系统。
以下是可以运行ARC代码的设备必须满足的条件:
iOS4.0以上的移动设备或OS X10.6以上版本的64位系统的电脑;
归零弱引用需要iOS 5.0或OS X10.7以上版本的系统。
ARC只对可保留的对象指针有效。可保留的对象指针主要有以下三种:
1.代码块指针
2.Objective-C指针
3.通过attribute((NSObject))类型定义的指针
所有其他的指针类型,比如Char *和CF对象(例如CFStringRef)都不支持ARC特性。如果你使用的指针不支持ARC, 那么你将不得不亲自手动管理它们。ARC可以与手动的内存管理共同发挥作用。
如果你想要在代码中使用ARC, 必须满足以下三个条件:
1.能够确定哪些对象需要进行内存管理;
2.能够表明如何去管理对象;
3.有可行的办法传递对象的所有权。
第一个条件是对象的最上层集合知道如何去管理它的子对象。
第二个条件是你必须能够对某个对象的保留计数器的值进行加1或者减1的操作。也就是说所有NSObject类的子类都能进行内存管理。这包括了大部分你需要管理的对象。
第三个条件是在传递对象的时候,你的程序需要能够在调用者和接收者之间传递所有权。
使用ARC特性,从长远来看,可以帮助你减少烦恼,节省时间。
1.有时用weak会好一些
当用指针指向某个对象时,你可以管理它的内存(通过retain和release),也可以不管理。如果你管理了,就拥有对这个对象的强引用。如果你没有管理,那么你拥有的则是弱引用。如果你对一个属性使用了assign特性,你便创建了一个弱引用。
弱引用,有助于处理保留循环(引用循环,形成环)。强引用:被引用的对象的保留计数器的值加1;弱引用:被引用的对象的保留计数器的值不会增加。
解决引用循环问题的办法:采用弱引用(assign/weak)。
归零弱引用(特殊的弱引用)即让对象自己去清空弱引用的对象。在指向的对象被释放之后,这些弱引用就会被设置为零(即nil), 就可以像平常的指向nil值的指针一样被处理。归零弱引用只在iOS 5和OS X10.7以上的版本中有效。
对象的归零弱引用会在对象所指向的值失效的时候自动转换成nil。
如果想要使用归零弱引用,必须明确地声明它们。有两种方式可以声明归零弱引用:声明变量时使用__weak关键字或对属性使用weak特性。
如果你不想在支持弱引用的旧系统上使用ARC, 可以使用__unsafe__unretained关键字和unsafe_unretained特性,它们会告诉ARC这个特殊的引用是弱引用。
weak与assign的区别:
① weak关键字表明该属性定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。这一点特性与assign类似。然而在属性所指的对象遭到摧毁时,weak关键字修饰的属性值也会被清空(nil out),而assign关键字修饰的属性的设置方法只会针对“纯量类型”(scalar type, 或者说基本数据类型,例如CGFloat或NSInteger等)的简单赋值操作。
② assign主要用于基本数据类型,非OC对象,而weak必须用于OC对象。
使用ARC的时候有两种命名规则需要注意:
1.属性名称不能以new开头,比如说@property NSString *newString是不被允许的。
2.属性不能只有一个read-only而没有内存管理特性。如果你没有启用ARC, 可以使用@property(readonly)NSString *title语句,但如果你启用了ARC功能,就必须指定由谁来管理内存。因为默认的特性是assign。所以,你可以使用一个简单的修复,使用unsafe_unretained就可以了。
同样强引用也有自己的__strong关键字和strong特性。
需要注意,内存管理的关键字和特性是不能一起使用的,两者相互排斥。
2.一辆新车
Xcode提供了一个简单的步骤可以把我们已有的项目转换成支持ARC的。开始之前,必须确保垃圾回收机制没有使用,垃圾回收和ARC是无法一同使用的。
转换过程一共需要两步。首先必须确保你的代码能符合ARC的需求,然后执行转换操作。
ARC转换是一个单程的操作。一旦你转换成ARC版本,就不可以恢复了。
因为ARC是基于文件进行工作的,所以你可以选在在同一个项目中经过ARC转换和未经过转换的文件共存。
3.拥有者权限
指针支持ARC的一个条件是必须是可保留对象指针(ROP).这意味着,你不可能简单地将一个ROP表示成不可保留的对象指针(non-ROP), 因为指针的所有权会移交。
为了让ARC便于工作,需要告诉编译器哪个对象是指针的拥有者。为此可以使用一种被称为桥接转换(bridged cast)的C语言技术。这是一个标准的C语言类型转换,不过使用的是其他关键字:__bridge、__bridge_retained和__bridge_transfer。术语bridge指的是使用不同的数据类型达到同一目的的能力。
以下是对三种桥接转换类型的详细介绍:
NSString *theString = @”Learn Objective-C”;
CFStringRef cfString = (CFStringRef)theString;
1.(__bridge类型)操作符:这种类型的转换会传递指针但不会传递它的所有权。在上面的代码中,操作符是theString , 而类型是CFStringRef。如果你使用了这个关键字,则一个指针是ROP, 而另一个不是。在这种情况下指针的所有权仍会留在操作符上。
cfString = (__bridge CFStringRef)theString;
cfString接收了指令,但指针的所有权仍由theString保留。
2.(_bridge_retained类型)操作符:使用这种类型,所有权会转移到non-ROP上。与上一个相同,一个指针是ROP,另一个则不是。因为ARC只会注意到ROP, 所以你要在不用的时候释放它。这个转换类型会给对象的保留计数器加1,所以你必须要让它减1,这与标准的内存管理方式相同。
以下是使用了这种转换类型的代码示例:
cfString = (__bridge__retained CFStringRef)theString
在这个示例中,cfString字符串拥有指针并且它的保留计数器加1。你要使用retain和release来管理它的内存。
3.(__bridge_transfer类型)操作符: 这种转换类型与上一个相反,它把所有权交给ROP。在这个示例中,ARC拥有对象并确保它会像ARC对象一样得到释放。
另一个限制是结构体(struct)和集合体(union)不能使用ROP作为成员。
以下是不能对ARC管理的对象调用的管理方法:
retain
retainCount
release
autorelease
dealloc
因为你有时需要释放不支持ARC的对象或执行其他清理操作,所以仍要实现dealloc方法,但是不能直接调用[super dealloc]。
以下是不能对ARC对象进行重写的方法:
retain
retainCount
release
autorelease
1.3异常
什么是异常?异常就是意外事件,比如数组溢出,因为程序不知道怎么处理就会扰乱程序流程。
当发生这种情况时,程序可以创建一个异常对象,让它在运行时系统中计算出接下来该怎么做。Cocoa中使用NSException类来表示异常。Cocoa要求所有的异常必须是NSException类型的异常,虽然你可以通过其他对象抛出异常,但Cocoa并不会处理它们。此外,你也可以创建NSException的子类来作为你自己的异常。
异常处理的真正目的是处理程序中生成的错误。Cocoa框架处理错误的方式通常是退出程序。为了找到出错的原因,你应该抛出并捕捉代码中的异常。
在运行时系统中创建并处理异常的行为被称为抛出异常,或者说是提出异常。
处理被抛出的异常的行为被称为捕捉异常。
如果一个异常被抛出但没有捕捉到,程序会在异常断点处停止运行并通知有这个异常。
1.3.1与异常有关的关键字
异常的所有关键字都是以@开头的。以下是它们的各自作用。
@try: 定义用来测试的代码块以决定是否要抛出异常。
@catch(): 定义用来处理已抛出异常的代码块。接收一个参数,通常是NSEXception类型,但也可能是其他类型。
@finally: 定义无论是否有抛出异常都会执行代码块,这段代码总是会执行的。
@throw:抛出异常。
为了确保Cocoa能够正常处理异常,你应该只用NSException对象来抛出异常。虽然你也可以用其他对象(除了NSException实例以外)抛出异常,但并不是所有的Cocoa框架都会捕捉其他对象抛出的异常。
1.3.2捕捉不同类型的异常
你可以根据需要处理的异常类型使用多个@catch代码块。处理代码应该按照从具体到抽象的顺序排序,并在最后使用一个通用的处理代码。
C语言程序员经常会在异常处理代码中使用setjmp和longjmp语句。你不能使用setjmp和longjmp来跳出@try代码块,但可以使用goto和return语句退出异常处理代码。
1.3.3抛出异常
当程序检测到了异常,就必须向处理它的代码块(有时叫做异常处理代码)报告这个异常。通知异常的过程被称为抛出(或提出)异常。
程序会创建一个NSException实例来抛出异常,并会使用以下两种技术之一:
使用”@throw异常名:”语句来抛出异常;
向某个NSException对象发送raise消息。
两种方法都可以使用,但不要两种都使用。两种方法的区别是raise只对NSException对象有效,而@throw也可以用在其他对象上。
你通常会在异常处理代码中抛出异常。代码可以通过再发送一次raise消息或使用@throw关键字来通知异常。
在@catch异常处理代码中,你可以重复抛出异常而无需指定异常对象。
与当前@catch异常处理代码相关的@finally代码块会在@throw引发下一个异常处理调用之前执行代码,因为@finally是在@throw发生之前调用的。
Objective-C的异常处理机制与C++的异常机制兼容。
1.3.4异常也需要内存管理
如果代码中有异常,内存管理执行起来会比较复杂。
比如遇到一种常见的问题:在某一个对象未被释放之前,程序出现异常,从某一个方法中跳出并寻找异常处理代码。一种简单的解决办法就是使用@try和@finally代码块,因为@finally总是会执行的,所以它可以在里面进行清理工作。这种方式也可以用在C语言类型的内存管理上,比如malloc或free。
1.3.5异常和自动释放池
异常处理有时会遇到异常对象被自动释放的小问题。因为你不知道该什么时候释放,所以异常总是作为自动释放对象而创建。当自动释放池销毁了以后,自动释放池中所有的对象也会被销毁,包括异常。
比如遇到一种常见的问题:自动释放池的释放早于异常通知,而当异常再次被抛出的时候,它会变成可怕的僵尸异常。(即:异常对象已经被释放,而异常发生的时候会再次抛出异常。) 与之对应的简单的解决办法是:在自动释放池外保留异常对象,而在自动释放池被释放之后,它也会同当前池一同被释放。
内存管理,或者说ARC/MRC管理的范围: 管理所有OC对象,对基本数据类型无效,因为OC对象和其他数据类型在系统中存储的位置不一样,其他数据类型和局部变量主要存储于栈区(因为基本数据类型占用的存储空间是固定的,一般存放于栈区),而对象存储于堆中。当代码块结束时,代码块所涉及到的所有局部变量会自动弹栈清空,指向对象的指针也会被回收,这时对象就没有指针指向,但依然存在于堆内存中,造成内存泄露。即:对象存储于堆中,而指向对象的指针存储于栈中。
小结:
本章介绍了Cocoa的内存管理方法:retain、release和autorelease, 还讨论了垃圾回收和自动引用技术(ARC, 苹果最新的内存管理技术)
每个对象都维护一个保留计数器。对象被创建时,其保留计数器的值为1;对象被保留时,其保留计数器的值加1;对象被释放时,其保留计数器的值减1;当保留计数器的值归0时,对象被销毁。在销毁对象时,首选调用对象的dealloc方法,然后回收其占用的内存以供其他对象使用。
Cocoa有三个关于对象及其保留计数器的规则:
如果使用new、alloc或copy操作获得了一个对象,则该对象的保留计数器的值为1。
如果通过其他方法获得一个对象,则假设该对象的保留计数器的值为1,而且已经设置为自动释放。
如果保留了某对象,则必须保持retain方法和release方法的使用次数相等。
通常ARC会在编译过程中通过插入这些语句来帮助你执行保留或释放操作。

    原文作者:天山雪莲_38324
    原文地址: https://www.jianshu.com/p/9a8600711d2b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞