C语言自动计数功能,《Objective-C高级编程》温故知新之"自动引用计数"

前言

很久前看了《Objective-C高级编程 iOS与OS X多线程和内存管理》这本书,但当时看起来晦涩难懂。最近利用下班时间重读了一遍,觉得还是得记录一下。毕竟往后阶段对相同的东西会有更深刻的理解。温故知新!

系列文章:《Objective-C高级编程》温故知新之"自动引用计数"

《Objective-C高级编程》温故知新之"Blocks"

从自动引用计数概念开始

概念:自动引用计数是指内存管理中对内存管理中对引用采取自动计数的计数。

工具:Clang是一个C语言、C++、Objective-C、Objective-C++语言的轻量级编

Clang使用: clang -rewrite-objc (文件名)

说一下clang工具的使用。比如我有一个类叫dwyane.m。里面代码如下:int main(int argc, const char * argv[]) {

@autoreleasepool {

id __strong obj = [NSMutableArray array];

}

return 0;

}

id add()

{

id __strong obj2 = [[NSMutableArray alloc] init];

return obj2;

}

在终端,进入dwyane.m目录,clang -rewrite-objc dwyane.m ,然后,系统会为我们生成dwyane.cpp(C++文件),可以看到下列c++源码int main(int argc, const char * argv[]) {

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

id __attribute__((objc_ownership(strong))) obj = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));

}

return 0;

}

id add()

{

id __attribute__((objc_ownership(strong))) obj2 = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));

return obj2;

}

Objective-C中的内存管理

也就是引用计数。

文中利用开关灯事件解释得非常完美。引用数随着人员进屋离去随之加减。引用数0时关灯

f2d0d18ec0ab7e23f973d6ac4858e5b8.png

办公室的照明管理

转换到Objective-C程序中,其实就是下图

74e0585010981fb059029d2ebc71d0dc.png

引用计数的内存管理

内存管理的思考方式自己生成的对象,自己所持有。

非自己生成的对象,自己也能持有。

不再需要自己持有的对象时释放。

非自己持有的对象无法释放。

上面出现的“生成” “持有” “释放” 再加上个 “废弃” 对应的OC方法如下

2f5358a940d68bb5b83c041f602d3d1f.png

1、使用alloc、new、copy、mutableCopy的意味着自己生成的对象只有自己持有

eg:id obj = [NSObject alloc] init]; //自己生成并持有对象

其中自己可理解为“对象的使用环境”或者理解改变世界的程序员本身

2、用alloc、new、copy、mutableCopy外的方法取得的对象,因非自己生成并持有,so不是该对象的持有者。比如NSMutableArray类中的 array类方法

id obj = [NSMutableArray array]; //取得的对象存在,但自己不持有对象

使用retain可持有对象

[obj retain]; //这样跟上述的alloc等生成持有对象的方法就一样了。

3、自己持有的对象,不需要请用release释放对象

id obj = [NSMutableArray array]; //取得的对象存在,但自己不持有对象

[obj release]; //释放对象

指向对象的指针仍然被保留在变量obj中,貌似可访问,但对象一经释放,绝对不可访问。

命名规则:如果不是自己生成并持有的方法,不得用alloc、new、copy、mutableCopy开头的方法名。比如下面方法id add()

{

id  obj = [[NSObject alloc] init]; //自己生成并持有对象

[obj autorelease]; //释放,取得对象存在,但自己不持有对象

return obj;

}

autorelease使对象在超出指定的生存范围时能够自动并正确地释放(调用release方法),如图

0acfc3603443e49cd222443362039a0f.png

release 和 autorelease 的区别

4、无法释放非自己持有的对象,如果释放非自己持有的对象就会造成崩溃

alloc/retain/release/dealloc 实现

1、GNUstep的实现

由于NSObject类的源代码没有公开,所以借助与苹果的Cocoa框架类似的GNUstep来理解苹果的Cocoa实现。

GUNstep的中NSObject类的alloc类方法间接调用NSZoneMalloc函数来分配存放对象所需的内存空间,之后将内存空间置0,最后返回作为对象而使用的指针。

区域:NSZoneMalloc的NSZone是什么呢?它是为防止内存碎片化而引入的结构。堆内存分配本身进行多重化管理,根据使用对象的目的、对象的大小分配内存,从而提高了内存管理的效率。

但是现在运行时系统只是简单地忽略区域的概念。运行时系统中的内存管理本身已极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。

26e2c5f66605d3c088c6bd0a34c8c8f1.png

alloc类方法用struct obj_layout 中的 retain 整数来保存引用计数,并将其写入内存头部,该对象内存块全部置0后返回。过程如图

fd44009d6b0ecbce5817a3c6b7761cf5.png

对象的引用计数可通过 retainCount 实例方法取得(非ARC下)id obj = [[NSObject alloc] init];

NSLog(@"retainCount=%lu", (unsigned long)[obj retainCount]);

/** 结果为 retainCount=1 */

由此可见,执行 alloc 后对象 retainCount 是 “1”。可以通过GNUstep的源代码确认一下

0c3bdfd91d6ed0619fd7772fb66b4fe0.png

retainCount源代码

由对象寻址到对象内存头部,从而访问其中的 retained 变量。

bc35a4a127d94f97816a701066ff1006.png

通过对象访问对象内存头部

因为分配时全部置0,所以 retained 为0.由 NSExtaRefCount(self) + 1;得出,retainCount 为1.从而推测出,retain方法使 retained变量加1,而 release方法使 retained变量减1。

2、苹果的实现

alloc类方法首先调用allocWithZone:类方法,这和GNUstep的实现相同,然后调用class_createInstance 函数,最后通过调用 calloc 来分配内存块。class_createInstance 函数的源码可以通过obj4库中的源码进行确认

从源代码的函数来看,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。如图

7346c4d66c287861d79c138f85c138b6.png

GNUstep将引用计数保存在对象占用内存块头部的变量中,而苹果的实现,则是保存在引用计数表中的记录中。

CGUstep的实现和苹果的实现好处区别如下:

通过内存块头部管理引用计数的好处如下:少量代码即刻完成

能够统一管理引用计数用内存块与对象用内存块。

通过引用计数表管理计数的好处如下:对象用内存块的分配无需考虑内存块头部。

引用计数表各记录中存有内存块地址,可从各个记录追溯到各对象内存块。

其中第二条最重要。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能够确认各内存块的位置。如图

5719dc93170b8422d144221784ceb2f6.png

另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象的持有者是否存在。

autorelease

顾名思义,autorelease 就是自动释放,看起来像ARC,但实际上更类似C语言中的自动变量(局部变量)特性。

C语言的自动变量:程序执行时,某自动变量超过其作用域,该自动变量将自动被废弃。{

int a;

} //超过变量 a 的作用域,所以"()"外不可访问

区别在于 autorelease 可以被编程人员设定变量的作用域。

autorelease 的具体使用方法如下:生成并持有 NSAutoreleasePool 对象;

调用已分配对象的 autorelease 实例方法;

废弃 NSAutoreleasePool 对象

11f5c9718f62824c290b5fc0b89d39a6.png

用代码来表示上图流程NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

id obj = [[NSObject alloc] init];

[obj autorelease];

[pool drain]; //等同于 "[pool release]"

NSAutoreleasePool 对象的生存周期相当于C语言的作用域。**对于所有调用过 autorelease 实例方法的对象,在废弃 NSAutoreleasePool 对象时,都将调用 release 实例方法。

在大量产生 autorelease 的对象时, 只要不放弃 NSAutoreleasePool 对象,那么生成的对象就不能释放,因此有时会产生内存不足现象。eg:读入大量图像的同时改变其尺寸。图像文件读入到 NSData 对象,并从中生成新的 UIImage 对象。这种情况下,就会大量产生 autorelease 的对象。

6bbbbaa73a9d780a7a783155157ab288.png

所以,需要在适当的地方生成、持有或废弃 NSAutoreleasePool 对象。

af725c975d8fd1c483fb15ea418e453a.png

所有权修饰符__strong

__weak

__unsafe_unretained修饰符

__autoreleasing修饰符

“ __strong ” 修饰符

__strong 修饰符是id类型和对象类型默认的所有权修饰符。即下面等号左右两边相等

id obj = [NSObject alloc] init]; <==>id __strong obj = [NSObject alloc] init];

下面代码在ARC和非ARC状态下的样子

ARC:{

id __strong obj = [NSObject alloc] init];

}

非ARC:{

id obj = [NSObject alloc] init];

[obj release];

}

可以看出,为了释放生成并持有的对象,增加了调用release方法的代码。该源代码进行的动作同先前ARC有效时的动作完全一样。

如此源代码所示,__strong 修饰符修饰的变量obj在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。{

/** obj0 持有对象A的强引用 */

id __strong obj0 = [[NSObject alloc] init]; //对象A

/** obj1 持有对象B的强引用 */

id __strong obj1 = [[NSObject alloc] init]; //对象B

/** obj2不持有对象 */

id __strong obj2 = nil;

/** obj0 持有赋值给obj2 的对象B的强引用

*  同时obj0丢失原先对对象A的强引用,即

*  对象A的所有者不存在,所以废弃对象A

*  此时,持有对象B的强引用的变量为

*  obj0和obj1.

*/

obj0 = obj1;

/** obj2持有由obj0赋值的对象B的强引用

* 此时,持有对象B的强引用的变量为

* obj0, obj1和obj2。

*/

obj2 = obj0;

/** obj1对对象B的强引用失效,此时

* 持有对象B的强引用变量为 obj2.

*/

obj1 = nil;

/** 对对象B的强引用失效,对象B的所有者不存在,因此废弃对象B */

obj2 = nil;

}

**__strong修饰符的变量,不仅只在变量作用域,在赋值上也能够正确地管理其对象的所有者。@implementation Test

- (instancetype)init {

self = [super init];

return self;

}

- (void)setObject:(id __strong)obj {

obj_ = obj;

}

- (void)testMethod {

id __strong test = [[Test alloc] init]; //test生成并持有Test对象的强引用

[test setObject:[[NSObject alloc] init]]; //Test对象的obj_成员持有NSObject对象的强引用

/** 因为test变量超出其作用域,强引用失效,所以自动释放Test对象。Test对象的所以者不存在,所以废弃该对象。

* 废弃Test对象的同时,Test对象的成员obj_也被废弃,

*同时自动释放NSObject对象,NSObject对象的所有者不存在,所以废弃该对象 */

}

@end

“__weak ” 修饰符

看起来,苹果内存管理拥有__strong就足够,然而,不是这样的,遇到引用计数式内存管理中必然会发生的“循环引用”的问题,就需要用到 __weak 修饰符了

92ed1e18961d45ed8995913a24b624eb.png

循环引用

我们修改下上面例子testMethod函数的代码。- (void)testMethod {

id test0 = [[Test alloc] init]; //对象A

/** test0生成并持有Test对象A的强引用 */

id test1 = [[Test alloc] init]; //对象B

/** test1生成并持有Test对象B的强引用 */

[test0 setObject:test1]; //Test对象的obj_成员持有赋值给test1的Test对象B的强引用

/** 此时,持有Test对象B的变量有

* 对象A的obj_成员以及test1 */

[test1 setObject:test0]; //Test对象的obj_成员持有赋值给test1的Test对象的强引用

/** 此时,持有Test对象A的变量有

* 对象B的obj_成员以及test0 */

}

bb0b8044bc9ace6205a384114678ec3d.png

循环引用容易发生内存泄漏,所谓内存泄漏就是应当废弃对象在超出其生命周期后继续存在。

还有,只有一个对象,其持有其自身,也会内存泄漏。id test = [[Test alloc] init]; //生成并持有NSObject对象

[test setObject:test]; //NSObject对象被NSObject的obj_成员强引用

如图

227a0da70bb9e933bea1a7a6c92e5aae.png

接下来利用__weak修饰符解决循环问题,再修改上面例子{

id __strong obj0 = [[NSObject alloc] init]; //obj0生成并持有NSObject对象的强引用

id __weak obj1 = obj0;

/** obj1 变量持有 NSObject 的弱引用 */

}

/** 因为obj0 变量超出其作用域,强引用失效,所以自动释放自己持有NSObject对象

*  又因为__weak修饰符的变量(即弱引用)不持有对象

*  对象持有者全部不存在,所以被废弃

如图

2280060cfec07f0db6e9d7dc88bf9b49.png

__weak修饰符还有个优点:持有某对象弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态(空弱引用)

“__unsafe_unretained”修饰符

__weak 修饰符只能用于iOS5以上及OS X Lion以上版本的应用程序,在iOS4以及OS X Snow Leopard 的应用程序可使用 __unsafe_unretained 代替

__unsafe_unretained 修饰符是不安全的修饰符,尽管ARC式的内存管理是编译器的工作,但附有__unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。

“__autoreleasing 修饰符”

ARC有效时,用@autoreleasepool 块替代 非ARC的 NSAutoreleasePool 类,用附有 __autoreleasing 修饰符的变量替代autoreleasing 方法。如图

be4268524c4c739ce71a9d6f241ab4cd.png

注意:但是,显式地附加 __autoreleasing 修饰符同显式地附加 __strong 修饰符一样罕见。这是因为编译器会检查方法名是否以alloc/new/copy/utableCopy开始,如果不是则自动将返回值的对象注册到 autoreleasepool。比如+ (id) array

{

id obj = [[NSMutableArray alloc] init];

return obj;

}

而,在访问附有 __weak 修饰符的变量时,实际上必定要访问注册到autoreleasepool的对象。为什么?请先看下列代码id __weak obj1 = obj0;

NSLog(@"class = %@", [obj1 class]);

以下源代码与此相同id __weak obj1 = obj0;

id __autoreleasing tmp = obj1;

NSLog(@"class = %@", [tmp class]);

因为__weak 修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把要访问的对象注册到 autoreleasepool 中,那么在autoreleasepool块结束前都能确保变量存在。

**注意:最后一个可非显示 __autoreleasing 修饰符的例子,id *obj 我们可能会类推出 id __strong *obj,但结果却是 id __autoreleasing *obj。同样,NSObject **obj 则是 NSObject *__autoreleasing *obj。

ARC规则不能使用 retain/release/retain/autorelease

不能使用NSAllocateObject/NSDeallocateObject

须遵守内存管理的方法命名规则

不要显示调用dealloc

使用@autoreleasepool 块替代 NSAutoreleasePool

不能使用区域(NSZone)

对象型变量不能作为C语言结构体(struct/union)的成员

显示转换“id”和“void”

不要显示调用dealloc- (void)dealloc {

[super dealloc];

}

/** 这样会报错 */

对象型变量不能作为C语言结构体(struct/union)的成员struct Data {

NSMutableArray *array;

};

/** error:ARC forbids Objective-C objects in struct */

如果一定要把对象型变量加入到结构体成员中,可强制转换为 void * 或者附加 __unsafe_unretained修饰符struct Data {

NSMutableArray __unsafe_unretained *array;

};

显示转换“id”和“void”

非ARCid obj = [[NSObject alloc] init];

void *p = obj;

ARC下

则会报错Implicit conversion of Objective-C pointer type 'id' to C pointer type 'void *' requires a bridged cast

错误提示了我们,可用bridge,我们修改下代码即可,如下:id obj = [[NSObject alloc] init];

void *p = (__bridge void *)obj; //id 转 void *

id o = (__bridge id)p; //void * 转 o

注意:前关注下Objective-C 对象与 Core Foundation 对象的互换以及免费桥(Toll-Free Bridge)的使用

__bridge_retained 和 __bridge_transfer转换

__bridge_retained/** ARC: */

id obj = [[NSObject alloc] init];

void *p = (__bridge_retained void *)obj;

//等同于下面

/** 非ARC */

// __bridge_retained转变成了retain。变量obj和变量p同时持有对象。

id obj = [[NSObject alloc] init];

void *p = obj;

[(id)p retain];

__bridge_transfer/** ARC */

void *p = (__bridge_retained void *)[[NSObject alloc] init];

NSLog(@"class=%@", [(__bridge id)p class]);

(void)(__bridge_transfer id)p; //释放了p,跟[p release];相同

//等同于下面

/** 非ARC */

id p = [[NSObject alloc] init];

NSLog(@"class=%@", [p class]);

[p release];

Objective-C对象与 Core Foundation 对象

Core Foundation对象主要使用在用C语言编写的Core Foundation框架中,并使用引用计数的对象。在ARC无效时,CF的CFRetain/CFRelease对应retain/release

CF 对象和OC对象没有区别,所以在ARC无效时,用简单的C语言转换也能实现互换。另外这种互换不需要使用额外的CPU资源,因此被称为免费桥。

1、OC转CF//可用于toll-free bridge的互换

CFMutableArrayRef cfObject = NULL;

id obj12 = nil;

{

//obj持有对象A的强引用

id obj = [[NSMutableArray alloc] init]; //对象A

//cfObject也持有对象A的强引用

//            cfObject = (__bridge_retained CFMutableArrayRef)obj; //  等同于          cfObject = CFBridgingRetain(obj);

//注意: __bridge 不会对引用计数产生影响

cfObject = (__bridge CFMutableArrayRef)obj;

//            obj12 = obj; //obj2也持有对象A的强引用

CFShow(cfObject);

printf("reain count = %ld

", CFGetRetainCount(cfObject));

/** 打印:reain count = 1 */

}

//下面访问对象出错--》 出现悬垂指针

printf("retain count after the scope = %ld

", CFGetRetainCount(cfObject)); //对象的引用技术 :引用计数就是对一个对象记录其被引用的次数,其的引用计数可加可减

悬垂指针 :指向曾经存在的对象,但该对象已经不再存在了,此类指针称为悬垂指针。结果未定义,往往导致程序错误,而且难以检测。

2、CF转OC//生成并持有对象

CFMutableArrayRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);

printf("retain count = %ld

", CFGetRetainCount(cfObject));

/** 打印:retain count = 1 */

//通过CFBridgingRelease赋值,变量obj持有对象强引用的同时,对象调用CFRelease释放,相当于调用了(__bridge_transfer id)cfObject

id obj = CFBridgingRelease(cfObject);  //After using a CFBridgingRetain on an NSObject, the caller must take responsibility for calling CFRelease at an appropriate time.

//        id obj = (__bridge_transfer id)cfObject;  //(__bridge_transfer id)X

//cfObject上面已经被释放,你会奇怪为什么还有,这不是悬垂指针吗?其实不是,因为0bj继续持有对对象的强引用,所以cfObject也指向仍然存在的对象,可以正常使用

printf("retain count after the cast = %ld

", CFGetRetainCount(cfObject));

/** 打印:retain count after the cast = 1 */

属性

ARC有效时,以下可作为属性声明中使用的属性来用。

2cac9c2b2842492e326638306b01b69d.png

书原文中写道:在声明类成员变量时,如果同属性声明中的属性不一致则会引起编译错误。比如@property (nonatomic, weak) id obj1;

需要改成@property (nonatomic, weak) id __weak obj1;

又或者把属性声明改成strong@property (nonatomic, strong) id obj1;

但经笔者试验,在Xcode V9.2 、macOS 10.12.6 下编译运行成功,并无报错

数组

id __strong *array = nil;

注意:id *类型 默认为”id __autoreleasing *“类型,所以需要显式指定为__strong修饰符。另外,上式虽然保证了附有__strong修饰符的id型变量被初始化为nil,但并不能保证附有__strong修饰符的id指针型变量被初始化为nil。

在动态数组中操作附有__strong修饰符的变量与静态数组有很大差异,需要自己释放所有的元素。如下源码,在只是简单地用free函数废弃了数组用内存块的情况下,数组各元素所赋值的对象不能再次释放,从而引起内存泄漏。

free(array)

这是因为在静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从处理。所以一定要将对象赋值nil,使元素所赋值对象强引用失效,从而释放对象,再free函数废弃内存块for (NSUInteger i = 0; i 

array[i] = nil;

free(array);

ARC的实现

1、__strong修饰符的实现{

id __strong obj = [[NSObject alloc] init];

}

上面代码如何运行呢?看看汇编和苹果源码obj4库,大概知道程序是如何工作的。下面请看编译器的模拟源代码

28f51e14a806ffa55656db990be91783.png由图可知,2次调用了obj_msgSeng 方法(alloc 和 init 方法),变量作用域结束时通过 objc_release 释放对象(编译器自动插入了release)

_objc_retainAutoreleasedReturnValue函数主要用于最优化程序运行。顾名思义,它是用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在autoreleasepool中对象的方法,或者是函数的返回值。

_objc_autoreleaseReturnValue与之相对应,用于NSMutableArray类的array类方法等返回对象的实现上。

注意:_objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用_objc_retainAutoreleasedReturnValue()函数,那么就不将返回的对象注册到autoreleasepool中,而是直接传递到方法或函数的的调用方。_objc_retainAutoreleasedReturnValue()函数与obj_retain函数不同,它即便不注册到autoreleasepool中而返回对象,也能正确的获取对象。

d15acf374ac5fbf1eafa66b144c75aa9.png

2、__weak 修饰符的实现若附有__weak修饰符的变量所引用的对象被废弃,则将nil赋值给改变量。

使用附有__weak修饰符的变量,即是使用注册到autoreasepool中的对象。

那他们是如何实现的呢?请看下列代码{

id __weak obj1 = obj;

}

下面请看编译器的模拟源代码

62c8be890896db895d689670fecda711.png

那具体如何实现上图的操作,请继续看源码

d784682405b997c8b52d3bd35973753c.png

cec33734a92f0e691510cad991403fd3.png

objc_storeWeak 函数把第二参数的赋值对象的地址作为建值,将第一参数的附有__weak修饰符的变量的地址注册到 weak 表中。如果第二参数为0,则将变量的地址从weak 表从 weak 表中删除。

weak 表与引用计数表相同,作为散列表被实现。如果大量使用附有 __weak 修饰符的变量,则会消耗相应的 CPU 资源。良策是只在需要避免循环引用时才使用 __weak 修饰符{

id __weak obj = [[NSObject alloc] init];

}

但上面会引起编译器警告,因为__weak修饰,NSObject 没有所有者,创建后,马上就通过 objc_release 函数被废弃。

我们看下下列代码,验证功能

使用附有__weak修饰符的变量,即是使用注册到autoreasepool中的对象。{

id __weak obj1 = obj;

NSLog(@"%@", obj1);

}

源代码如下:

cf637680ef5c02bcb1f0753c7c1eb5d0.png

由此可知,因为附有__weak修饰符变量所引用的对象像这样被注册到autoreleasepool中,所以在@autoreleasepool块结束前之前都可以放心使用。但大量使用__weak修饰的变量,

注册到autoreleasepool的对象也会大量增加,最好先暂时赋值给__strong修饰符的变量后再使用。id obj = [[NSObject alloc] init];

id __weak obj1 = obj;

NSLog(@"1 = %@", obj1);

NSLog(@"2 = %@", obj1);

NSLog(@"3 = %@", obj1);

NSLog(@"4 = %@", obj1);

上面变量obj所赋值的对象也就注册到autoreleasepool4次

建议使用:id obj = [[NSObject alloc] init];

id __weak obj1 = obj;

id tmp = obj1;

NSLog(@"1 = %@", tmp);

NSLog(@"2 = %@", tmp);

NSLog(@"3 = %@", tmp);

NSLog(@"4 = %@", tmp);作者:Dwyane_Coding

链接:https://www.jianshu.com/p/6dbdd8cb93d2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值