ARC下 Dealloc常见错误及其使用

Dealloc怎么写(dealloc的方法实现建议放的位置)

推荐的代码组织方式是将dealloc方法放在实现文件的最前面(直接在@synthesize以及@dynamic之后)

顺便提一下,@synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法,当然定义一个属性的时候,系统会默认编写了,而 @dynamic 的用法则相反,属性的 setter 与 getter 方法由用户自己实现,不自动生成。

在dealloc方法中通常需要做的有移除通知或监听操作,或对于一些非Objective-C对象也需要手动清空,比如CoreFoundation中的对象。

MRC下就要手动释放、置空变量等操作后还需要调用父类的dealloc,而ARC下除了CoreFoundation的对象需要手动释放以及KVO监听移除外(NSNotification 在iOS9之后也不需要手动移除了),基本就没了,当然ARC的内存销毁具有一定的滞后性,也可将一些变量手动置空,也就是告诉系统这些变量已经使用完毕可以释放了,当然也可以不做任何操作,系统会自动释放这些成员变量或者属性。

MRC下dealloc 方法

在MRC中dealloc方法存在的主要意义:
释放自身的实例变量,
移除观察者,
停止timer,
移除通知,
代理置空等。
注意MRC 下dealoc 方法一定要在最后写
[super dealloc];

ARC下系统会帮助我们释放该对象所包含的实例变量,但是有些对象还是需要们自己去释放的(比如Core Foundation框架下的一些对象),另外通知中观察者的移除,代理置空,停止timer等;

示例如下所示: 一定不能有 [super dealloc];

- (void)dealloc{    
[[NSNotificationCenterdefaultCenter] removeObserver:self];//移除通知观察者
[[XMPPManager sharedManager] removeFromDelegateQueue:self];//移除委托引用
[[MyClass shareInstance]  doSomething ]//其他操作
scrollView.delegate=nil;    
[timer invalidate]; 
 }
ARC 对于Core Foundation对象的内存管理是无效,需要手动添加CFRelease、CFRetain消息

对于CFRelease、CFRetain可以直观认为与Object-C中的retain、release等价。因此对于底层Core Foundation对象,依然需要手动引用计数来管理内存。

    // 创建 CFStringRef 对象
    CFStringRef strRef = CFStringCreateWithCString(kCFAllocatorDefault, "hello world", kCFStringEncodingUTF8);
    // 创建
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", 12, NULL);
    
    // 引用计数加一
    CFRetain(strRef);
    CFRetain(fontRef);
    
    // 引用计数减一
    CFRelease(strRef);
    CFRelease(fontRef);

Dealloc常见错误

dealloc中置空操作
- (void)dealloc {
NSLog(@"BaseModel dealloc");
self.baseName = nil;
}

这个表面看上去没什么问题,在《Effective Objective-C 2.0》一书中第7条提到:

]在对象内部尽量直接访问实例变量,而在初始化方法和dealloc方法中,总是应该直接通过实例变量来读写数据。

首先self.name意为调用属性name的setter方法进行赋值

-(void)setName:(NSString*)name { 
 	_name = name;
 }

即将nil作为参数传递到setName:方法中,其方法内部本身执行的仍为_name = nil,乍看来和手动在dealloc中书写_name = nil没有什么区别,但是setter方法并没有看起那样简单,若在MRC中通常setter方法如下所示:

- (void)setName:(NSString*)name { 
	if (_name != name) {
		[_name release];
		 _name = [name retain]; 
	}
}

对于setter赋值方法采用的为释放旧值保留新值的方式。直接调用_name=nil避免了指针转移问题且避免了Objcetive-C的“方法派发”操作,因此直接调用_name=nil相比self.name=nil执行效率会高一些。

除了文中所说的加快访问速度之外,但是如果用法不巧当的话,会出现不必要的崩溃问题。下面举个简单的例子分析一下:

定义了一个BaseModel 基类,基类中演示了使用self.baseName = nil:

// BaseModel.h

@interface BaseModel : NSObject
@property (nonatomic, copy) NSString * _Nullable baseName;
@end

// BaseModel.m
#import "BaseModel.h"
@implementation BaseModel
- (void)dealloc {
    NSLog(@"BaseModel dealloc");
    self.baseName = nil;
}

- (void)setBaseName:(NSString *)baseName {
    _baseName = baseName;
    NSLog(@"BaseModel setBaseName:%@", baseName);
}
@end

同时定义了一个子类SubModel继承自BaseModel,子类中重写了baseName 的setter方法,并获取baseName进行其他操作

#import "SubModel.h"
@implementation SubModel
- (void)dealloc {
NSLog(@"SubModel dealloc");
}

- (void)setBaseName:(NSString *)baseName {
    [super setBaseName:baseName];
NSLog(@"SubModel setBaseName:%@", [NSString stringWithString:baseName]);
    /**
      [NSString stringWithString:baseName]
     原因是[NSString stringWithString:baseName] 这里,baseName是nil,而这个方法是不允许传nil参数的,
     Summary
     Returns a string created by copying the characters from another given string.
     
     Declaration
     + (instancetype)stringWithString:(NSString *)string;
     Parameters

     aString
     The string from which to copy characters. This value must not be nil.
     Important
     Raises an NSInvalidArgumentException if aString is nil.
     Returns

     A string created by copying the characters from aString.
     
     
     */
}
@end
//VC.m
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
  SubModel * m = [[SubModel alloc]init];
    [m setBaseName:@"mhf"];
    
    /**
     打印结果
     2020-05-16 11:14:04.818275+0800 OCEssence[4731:144880] BaseModel setBaseName:mhf
     2020-05-16 11:14:52.482241+0800 OCEssence[4731:144880] SubModel setBaseName:mhf
     2020-05-16 11:15:04.793489+0800 OCEssence[4731:144880] SubModel dealloc
     2020-05-16 11:15:07.099555+0800 OCEssence[4731:144880] BaseModel dealloc
     2020-05-16 11:15:33.353289+0800 OCEssence[4731:144880] BaseModel setBaseName:(null)
     2020-05-16 11:15:58.919928+0800 OCEssence[4731:144880] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSPlaceholderString initWithString:]: nil argument'
     
     
     2   Foundation                          0x00007fff2577d523 -[NSPlaceholderString initWithValidatedFormat:validFormatSpecifiers:locale:arguments:error:] + 0
     3   Foundation                          0x00007fff2577606b +[NSString stringWithString:] + 45
     4   OCEssence                           0x00000001096c4267 -[SubModel setBaseName:] + 135
     5   OCEssence                           0x00000001096cdf01 -[BaseModel dealloc] + 65
     6   OCEssence                           0x00000001096c41d4 -[SubModel dealloc] + 68
     7   libobjc.A.dylib                     0x00007fff514110d6 _ZN11objc_object17sidetable_releaseEb + 174
     8   OCEssence                           0x00000001096c4589 -[ViewController touchesBegan:withEvent:] 
     
     
     */
 }

当SubModel作为一个临时变量生成后赋值baseName,变量使用完后系统会自动回收,此时大家可以想想会发什么什么问题?
不难想到,此时会出现崩溃现象,原因是[NSString stringWithString:baseName] 这里,baseName是nil,而这个方法是不允许传nil参数的,当然,这个业务处理上肯定需要一个判空操作,我们先来分析一下为什么会是nil

子类SubModel被释放会调用子类的dealloc方法,然后会调用父类BaseModel的dealloc方法,此时父类中通过setter方法来赋值nil,而子类SubModel重写了,子类拿到nil来处理导致崩溃问题

究竟属性是否需要手动置空释放?实际上来说,是不需要手动释放的,因为dealloc中.cxx_destruct会处理。当然因为执行是有一定延迟性,为了节省资源,在确保属性没利用价值的时候可以手动清空。

dealloc中使用__weak

举个简单例子来模拟实际复杂业务场景:

// SubModel.m
#import "SubModel.h"

@implementation SubModel

- (void)dealloc {
    NSLog(@"SubModel dealloc");
    [self performSelectorWhenDealloc];
}

-(void)performSelectorWhenDealloc {
    __weak typeof(self) weakSelf = self;
   // 模拟复杂的block结构,需要弱引用解除循环引用
    void(^block)(void) = ^{
        [weakSelf test];
    };
    block();
}

- (void)test {
    NSLog(@"test");
}

@end


//VC.m
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
  SubModel * m = [[SubModel alloc]init];
    [m setBaseName:@"mhf"];
}
/** 打印结果
 2020-05-16 12:09:04.536539+0800 OCEssence[5430:171885] SubModel dealloc
objc[5430]: Cannot form weak reference to instance (0x600000ab60b0) of class SubModel. It is possible that this object was over-released, or is in the process of deallocation.

当SubModel这个类被释放,调用dealloc的时候会出现崩溃,崩溃信息如下:Cannot form weak reference to instance (0x2813c4d90) of class xxx. It is possible that this object was over-released, or is in the process of deallocation.
崩溃图片
崩溃原因我们来分析一下:

先了解一下__weak 到底做了什么操作,通过clang 转换的代码是这样的 attribute((objc_ownership(weak))) typeof(self) weakSelf = self; 这样还是看不出问题,我们看回堆栈,堆栈崩在objc_initWeak 函数中,我们可以看看Runtime 源码 objc_initWeak 函数的定义是怎么样的:

id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

可以留意到,内部调用了 storeWeak 函数,其中有个模板名称是DoCrashIfDeallocating 不难猜到,当调用到了 storeWeak 函数的时候,如果释放过程中存储,那就会crash,函数最终会调用register函数 id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating)

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id, bool crashIfDeallocating)
{
...
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
...
}

在这就找到了崩溃时打印出信息了。通过上面的分析,我们也知道,__weak 其实就是会最终调用objc_initWeak 函数进行注册。抱着求学的态度,可以在clang 8.7 对objc_initWeak函数描述中找到答案:

object is a valid pointer which has not been registered as a __weak object. value is null or a pointer to a valid object. If value is a null pointer or the object to which it points has begun deallocation, object is zero-initialized. Otherwise, object is registered as a __weak object pointing to value. Equivalent to the following code:
object是一个有效的指针,尚未注册为__weak对象。 value为null或指向有效对象的指针。如果value是一个空指针或它所指向的对象已开始释放,则将该对象初始化为零。否则,将对象注册为指向值的__weak对象。

dealloc中使用GCD

GCD相信大家平时用得不少,但在Dealloc方法里面使用GCD大家有没有注意呢,先来举个简单例子,我们在主线程中创建一个定时器,然后类被释放的时候销毁定时器,相关代码如下

- (void)dealloc {
[self invalidateTimer];
}

- (void)fireTimer {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
if (!weakSelf.timer) {
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"TestDeallocModel timer:%p", timer);
}];
[[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSRunLoopCommonModes];
}
});
}

- (void)invalidateTimer {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.timer) {
NSLog(@"TestDeallocModel invalidateTimer:%p model:%p", self->_timer, self);
[self.timer invalidate];
self.timer = nil;
}
});
}

补充说明一下,定时器的释放和创建必须在同一个线程,这个也是比较容易犯的错误点,官方描述如下:

Stops the timer from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point. If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
简单解释一下,当前的定时器销毁只能从启动定时器的Runloop中移除,然后Runloop和线程是一一对应的,因此需要确保销毁和创建在同一个线程中处理,否则可能会出现释放不了的情况。

说回正题,当定时器所在类被释放后,此时调用invalidateTimer 方法去销毁定时器的时候就会出现崩溃情况。

崩溃报错:Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

出现访问野指针问题了,原因其实不难想到程序代码默认是在主线程主队列中执行,而dealloc中异步执行主队列中释放定时器释放,GCD会强引用self,此时dealloc已经执行完成了,那么self 其实已经被free释放掉了,此时销毁内部再调用self就会访问野指针。

我们来继续分析一下,GCD为啥会强引用self,以及简单分析一下GCD的调用时机问题。

强引用问题,简单转换如下代码

- (void)dealloc {
dispatch_async(dispatch_queue_create("Kong", 0), ^{
[self test];
});
}
struct __TestModel__dealloc_block_impl_0 {
struct __block_impl impl;
struct __TestModel__dealloc_block_desc_0* Desc;
TestModel *const __strong self;
__TestModel__dealloc_block_impl_0(void *fp, struct __TestModel__dealloc_block_desc_0 *desc, TestModel *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __TestModel__dealloc_block_func_0(struct __TestModel__dealloc_block_impl_0 *__cself) {
TestModel *const __strong self = __cself->self; // bound by copy

((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("test"));
}


static void __TestModel__dealloc_block_copy_0(struct __TestModel__dealloc_block_impl_0*dst, struct __TestModel__dealloc_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}



static void __TestModel__dealloc_block_dispose_0(struct __TestModel__dealloc_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}



static struct __TestModel__dealloc_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestModel__dealloc_block_impl_0*, struct __TestModel__dealloc_block_impl_0*);
void (*dispose)(struct __TestModel__dealloc_block_impl_0*);
} __TestModel__dealloc_block_desc_0_DATA = { 0, sizeof(struct __TestModel__dealloc_block_impl_0), __TestModel__dealloc_block_copy_0, __TestModel__dealloc_block_dispose_0};

static void _I_TestModel_dealloc(TestModel * self, SEL _cmd) {
dispatch_async(dispatch_queue_create("Kong", 0), ((void (*)())&__TestModel__dealloc_block_impl_0((void *)__TestModel__dealloc_block_func_0, &__TestModel__dealloc_block_desc_0_DATA, self, 570425344)));
}

可以发现,dealloc 方法 转换成 static void _I_TestModel_dealloc(TestModel * self, SEL _cmd) 内部调用的dispatch 方法变化不大,主要是看Block的传递,可以留意到__TestModel__dealloc_block_impl_0 这个结构体地址参数,看其代码实现可以发现,TestModel *const __strong self; 就是这个__strong 使得Block 会对self 进行强引用。顺带说一下,结构体中有个struct __block_impl impl; 成员变量,而这个结构体内部有个FuncPtr 成员,Block的调用实际上就是通过FuncPtr 来实现的。
以上就通过一个简单的例子,解释了dealloc 中使用GCD中出现的问题

原理是一样的,简单总结一下:GCD任务底层通过链表管理,队列任务遵循FIFO模式,那么任务执行肯定就会有延迟性,同一时刻只能执行一个任务,只要dealloc任务执行先,那么此时block使用self就会访问野指针,因为dealloc内会有free操作。

Dealloc 源码分析
1、为什么dealloc 中使用GCD 会容易访问野指针?

出现野指针访问,那肯定就有free操作,我们查一下这个free是在哪一步执行:

//NSObject.mm
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}

最终调用在 objc-object.h 的 inline void objc_object::rootDealloc() 函数中

inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return;  // fixme necessary?

if (fastpath(isa.nonpointer  &&  // 是否是优化过的isa
!isa.weakly_referenced  &&  // 不包含或者不曾经包含weak指针
!isa.has_assoc  &&  // 没有关联对象
!isa.has_cxx_dtor  &&  // 没有c++析构方法
!isa.has_sidetable_rc))// 引用计数没有超出上限的时候可以快速释放,rootRetain(bool tryRetain, bool handleOverflow) 中设置为true
{
assert(!sidetable_present());
free(this);
} 
else {
object_dispose((id)this);
}

可以看到满足一定条件下,对象指针会直接free释放掉,实际很多情况下都会走object_dispose((id)this) 函数,这个函数是在objc-runtime-new.mm文件,下面继续分析这个函数实现

id 
object_dispose(id obj)
{
if (!obj) return nil;

objc_destructInstance(obj);
/// 释放内存
free(obj);

return nil;
}

终于看到了大概调用结构了,objc_destructInstance 函数后面分析,可以发现dealloc内部最终都会走free 操作,而这个操作就会导致野指针访问问题

1、为什么属性或者说成员变量会自动释放

开篇说了系统会自动释放属性或者成员变量,其实就是objc_destructInstance 函数的处理,其定义如下:

void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
// 对象拥有成员变量时编译器会自动插入.cxx_desctruct方法用于自动释放,可打印方法名证明;
if (cxx) object_cxxDestruct(obj);
// 移除关联对象
if (assoc) _object_remove_assocations(obj);
// weak->nil
obj->clearDeallocating();
}

return obj;
}

代码上我已经部分注释了,我们直接看object_cxxDestruct 函数实现,后面_object_remove_assocations 是移除关联对象,就是我们分类中通过objc_setAssociatedObject 函数新增的就是关联对象,而clearDeallocating 则是把对应weak哈希表的置空

void object_cxxDestruct(id obj)
{
if (!obj) return;
// 如果是isTaggedPointer,不处理

///为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念,避免32位机器迁移到64位机器内存翻倍 https://blog.devtang.com/2014/05/30/understand-tagged-pointer
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);

// Call cls's dtor first, then superclasses's dtors.
// 按继承链释放
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
/// .cxx_destruct是编译器生成的代码,在.cxx_destruct进行形如objc_storeStrong(&ivar, null)的调用后,对应的实例变量就被release和设置成nil了
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
// 进行过动态方法解析后会标记IMP为_objc_msgForward_impcache,进行缓存后会进行消息分发
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s", 
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}

看最终实现可以发现,内部就是通过继承链遍历调用lookupMethodInClassAndLoadCache 函数来进行后续释放,实际上是通过SEL_cxx_destruct 来执行C++的析构方法

简单总结一下dealloc的处理逻辑

object_cxxDestruct(obj)(释放成员变量) -> _object_remove_associations(obj) (移除关联对象) -> obj->clearDeallocating()(weak对象置为nil) -> free(obj)(free内存)

Dealloc 用法总结

1、dealloc中尽量直接访问实例变量来置空。

2、dealloc中切记不能使用__weak self。

3、dealloc中切线程操作尽量避免使用GCD,可利用performSelector,确保线程操作先于dealloc完成。

Dealloc 机制应用

从源码中可以知道,dealloc在对象置nil以及free之前,会进行关联对象释放,那么可以利用关联对象销毁监听dealloc完成,做一些自动释放操作,例如通知监听释放等,实际上网上也是有一些例子了。


@interface TestDeallocAssociatedObject : NSObject

- (instancetype)initWithDeallocBlock:(void (^)(void))block;

@end

@implementation TestDeallocAssociatedObject {
void(^_block)(void);
}

- (instancetype)initWithDeallocBlock:(void (^)(void))block {

if (self = [super init]) {
self->_block = [block copy];
}
return self;
}

- (void)dealloc {
if (self->_block) {
self->_block();
}
}

@end

然后在需要监听的地方创建关联对象,Block内处理即可,此时要注意Block引用的问题。

// 添加关联对象
TestDeallocAssociatedObject *object = [[TestDeallocAssociatedObject alloc] initWithDeallocBlock:^{
NSLog(@"TestDeallocAssociatedObject dealloc");
}];
objc_setAssociatedObject(self, &KTestDeallocAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
造成ViewController不释放的原因可能有很多。遇到dealloc不调用的时候只需要检查您的ViewController中是否存在以下几个问题:

ViewController中存在NSTimer
ViewController中有Block

常用第三方中dealloc方法
//AFNetworkActivityIndicatorManager.m
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [_activityIndicatorVisibilityTimer invalidate];
}
// SDWebImageDownloader.m
- (void)dealloc {
    [self.session invalidateAndCancel];
    self.session = nil;

    [self.downloadQueue cancelAllOperations];
    SDDispatchQueueRelease(_barrierQueue);
}
//FMDataBase.m
- (void)dealloc {
    [self close];
    FMDBRelease(_openResultSets);
    FMDBRelease(_cachedStatements);
    FMDBRelease(_dateFormat);
    FMDBRelease(_databasePath);
    FMDBRelease(_openFunctions);
    
#if ! __has_feature(objc_arc)
    [super dealloc];
#endif
}
//SocketRocket/SRProxyConnect.m
- (void)dealloc
{
    // If we get deallocated before the socket open finishes - we need to cleanup everything.

    [self.inputStream removeFromRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];
    self.inputStream.delegate = nil;
    [self.inputStream close];
    self.inputStream = nil;

    self.outputStream.delegate = nil;
    [self.outputStream close];
    self.outputStream = nil;
}
其他
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];

    dispatch_sync(self.taskQueue, ^{
        if (self.componetInstance) {
            self.isRunning = NO;
            AudioOutputUnitStop(self.componetInstance);
            AudioComponentInstanceDispose(self.componetInstance);
            self.componetInstance = nil;
            self.component = nil;
        }
    });
}

- (void)dealloc {
    [UIApplication sharedApplication].idleTimerDisabled = NO;
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [_videoCamera stopCameraCapture];
    if(_gpuImageView){
        [_gpuImageView removeFromSuperview];
        _gpuImageView = nil;
    }
}

- (void)dealloc{
    [self removeObserver:self forKeyPath:@"isSending"];
}

- (void)dealloc;
{
    free(rawImagePixels);
    free(cornersArray);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值