10个迷惑新手的Cocoa&Objective-c…

本文详细解析了MacOS开发中的FirstResponder概念及其在事件响应链中的作用,并深入探讨了从RC到ARC的内存管理进化,以及Objective-C中的类别与扩展。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

6. event respon­der

Interface Builder First Responder

Interface Builder First Responder

使用过Xcode的开发者都知道Interface Builder这个开发组件,在Xcode4版本以后该组件已经和xcode整合到一起。它是苹果软件开发中非常重要的部分。ib为开发者减轻了很大一部分界面设计工作。但是其中有一个东西让新接触ib的开发者一头雾水,那就是First Responder, 它是什么东西,为何它会有那么多Actions。这节我会详细介绍如何理解Responder和Cocoa下的事件响应链。

First Responder在IB属性为Placeholders,这意味着它属于一个虚拟实例。就好比TextField里面的string placeholder一样,只是临时显示一下。真正的first responder会被其它对象代替。实际上,任何派生自NSResponder类的对象都可以替代First Responder。而First Responder里面的所有Actions就是NSResponder提供的接口函数,当然你也可以定义自己的响应函数。

MacOS在系统内部会维护一个称为“The Responder Chain”的链表。该列表内容为responder对象实例,它们会对各种系统事件做出响应。最上面的哪个对象就叫做first responder,它是最先接收到系统事件的对象。如果该对象不处理该事件,系统会将这个事件向下传递,直到找到响应事件的对象,我们可以理解为该事件被该这个对象截取了。

The Responder Chain基本结构如下图所示:

The Responder Chain

The Responder Chain

在理解了上面的概念之后,我希望使用一个例子让大家对responder有更加具体的认识。大家都知道NSTextField这个控件,它是最常见的控件之一。它最基本功能是显示一个字符串,如果启用可选,那么用户可以选中文本,拷贝文本,如果开启编辑选项,还可以运行用户编辑文本,等等基本操作。

下面展示给大家的例子是创建一个我们自己创建的简单textfield叫LXTextField。它不属于NSTextField而是派生自NSView,具有功能显示字符串,全选字符串,响应用户cmd+c的拷贝操作,三个基本功能。注意NSView派生自NSResponder。

Objective-C
@interface LXTextField : NSView { NSString *stringValue; BOOL selectAll; } @property(retain,nonatomic) NSString *stringValue; @end #!objc // // LXTextField.m // lxtextfield // // Created by xu lian on 12-03-09. // Copyright (c) 2012 Beyondcow. All rights reserved. // #import "LXTextField.h" @implementation LXTextField @synthesize stringValue; - (void)awakeFromNib { selectAll = NO; } - (id)initWithFrame:(NSRect)frameRect { if( self = [super initWithFrame:frameRect] ){ selectAll = NO; } return self; } - (BOOL)acceptsFirstResponder { return YES; } - (BOOL)becomeFirstResponder { return YES; } - (BOOL)resignFirstResponder { selectAll=NO; [self setNeedsDisplay:YES]; return YES; } - (void)setStringValue:(NSString *)string{ stringValue = string; [self setNeedsDisplay:YES]; } - (void)drawRect:(NSRect)dirtyRect { if (selectAll) { NSRect r = NSZeroRect; r.size = [stringValue sizeWithAttributes:nil]; [[NSColor selectedControlColor] set]; NSRectFill(r); } [stringValue drawAtPoint:NSZeroPoint withAttributes:nil]; } - (IBAction)selectAll:(id)sender; { selectAll=YES; [self setNeedsDisplay:YES]; } - (IBAction)copy:(id)sender; { NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; [pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; [pasteBoard setString:stringValue forType:NSStringPboardType]; } - (void)mouseDown:(NSEvent *)theEvent { if ([theEvent clickCount]>=2) { selectAll=YES; } [self setNeedsDisplay:YES]; } - (void)keyDown:(NSEvent *)theEvent { } @end
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//
//  LXTextField.h
//  lxtextfield
//
//  Created by xu lian on 12-03-09.
//  Copyright (c) 2012 Beyondcow. All rights reserved.
//
 
#import
 
@interface LXTextField : NSView
{
     NSString *stringValue ;
     BOOL selectAll ;
}
@property ( retain , nonatomic ) NSString *stringValue ;
 
@end
 
 
 
#!objc
//
//  LXTextField.m
//  lxtextfield
//
//  Created by xu lian on 12-03-09.
//  Copyright (c) 2012 Beyondcow. All rights reserved.
//
 
#import "LXTextField.h"
 
@implementation LXTextField
@synthesize stringValue ;
 
- ( void ) awakeFromNib
{
     selectAll = NO ;
}
 
- ( id ) initWithFrame : ( NSRect ) frameRect
{
     if ( self = [ super initWithFrame :frameRect ] ) {
         selectAll = NO ;
     }
     return self ;
}
 
- ( BOOL ) acceptsFirstResponder
{
     return YES ;
}
 
- ( BOOL ) becomeFirstResponder
{
     return YES ;
}
 
- ( BOOL ) resignFirstResponder
{
     selectAll = NO ;
     [ self setNeedsDisplay :YES ] ;
     return YES ;
}
 
 
- ( void ) setStringValue : ( NSString * ) string {
     stringValue = string ;
     [ self setNeedsDisplay :YES ] ;
}
 
- ( void ) drawRect : ( NSRect ) dirtyRect
{
     if ( selectAll ) {
         NSRect r = NSZeroRect ;
         r . size = [ stringValue sizeWithAttributes :nil ] ;
         [ [ NSColor selectedControlColor ] set ] ;
         NSRectFill ( r ) ;
     }
     [ stringValue drawAtPoint :NSZeroPoint withAttributes :nil ] ;
}
 
- ( IBAction ) selectAll : ( id ) sender ;
{
     selectAll = YES ;
     [ self setNeedsDisplay :YES ] ;
}
 
- ( IBAction ) copy : ( id ) sender ;
{
     NSPasteboard *pasteBoard = [ NSPasteboard generalPasteboard ] ;
     [ pasteBoard declareTypes : [ NSArray arrayWithObjects :NSStringPboardType , nil ] owner :nil ] ;
     [ pasteBoard setString :stringValue forType :NSStringPboardType ] ;
}
 
- ( void ) mouseDown : ( NSEvent * ) theEvent
{
     if ( [ theEvent clickCount ] >= 2 ) {
         selectAll = YES ;
     }
     [ self setNeedsDisplay :YES ] ;
}
 
- ( void ) keyDown : ( NSEvent * ) theEvent
{
}
 
@end

运行实例,可以看到随着LXTextField收到系统发送的becomeFirstResponder消息,LXTextField变成responder chain中的frist responder, 这时候可以理解为IB里的哪个First Responder虚拟实例被该LXTextField取代。这时候mainMenu上哪些菜单项,例如:全选(cmd+a), 拷贝(cmd+a)等事件都会最先发给当前这个LXTextField。一旦你的LXTextField实现了NSResponder的哪些默认函数,那么该对象就会截取系统事件。当然这些事件具体如何实现还是需要你自己写代码实现。例如这里的 – (IBAction)copy:(id)sender; 显然我手动实现了textfield的copy能力。

注意上述代码中我实现了一个空函数- (void)keyDown:(NSEvent *)theEvent 这意味着我们希望LXTextField截取键盘事件而不再传递给responder chain后续对象。当然,如果我们希望LXTextField响应特定键盘事件,而其他事件继续传给其他响应对象,我们可以编写如下代码。

Objective-C
1
2
3
4
5
6
7
8
- ( void ) keyDown : ( NSEvent * ) theEvent
{
     if ( condition ) {
         do something ;
     } else {
         [ super keyDown :theEvent ] ;
     }
}

待写…

7. mem­ory management

内存管理问题,也许是问得最多的问题了吧。

  1. 内存管理规则 Cocoa下程序开发内存管理使用ref­er­ence counting(RC)机制。从10.7以后apple开始推荐auto­matic ref­er­ence counting(ARC)机制。大家是否知道从旧时代的RC到ARC机制到底意味着什么呢?为什么ARC从开发速度,到执行速度和稳定性都要优于rc?

开发速度不言而喻,你少写很多release代码,甚至很少去操心这部分。

执行速度呢?这个还要从runtime说起,还记得我在第2点说得一句话么:“Runtime is everything between your each function call.”

RC有一个古老的内存管理哲学:谁分配谁释放。 通过counting来确定一个资源有几个使用者。道理很简单,但是往往简单的东西人却会犯错。从来没有一个程序员可以充满信心的说,我写得代码从来没有过内存泄露。这样来看,我们就更需要让程序可以自己处理这个管理机制,这就需要把这个机制放到runtime里。

所以RC->ARC就是把内存管理部分从普通开发者的函数中移到了函数外的runtime中。因为runtime的开发原型简单,逻辑层次更高,所以做这个开发和管理出错的概率更小。实际上编译器开发人员对这部分经过无数次测试,所以可以说用arc几乎不会出错。另外由于编译的额外优化,使得这个部分比程序员自己写得代码要快速很多。而且对于一些通用的开发模式,例如autorelease对象,arc有更优秀的算法保证autoreleasepool里的对象更少。

  1. RC规则 首先说一下rc是什么,r-Reference参照,引用 c-counting计数, rc就是引用计数。俗话说就是记录使用者的数量。 例如现在我有一个房间空着,大家可以进去随意使用,但是你进门前,需要给门口的计数牌子+1, 出门时候-1。 这时候这个门口的牌子就是该房间里的人数。一但这个牌子变为0我就可以把房间关闭。

这个规则可以让NSObject决定是不是要释放内存。当一个对象alloc时候,系统分配其一块内存并且object自动计数retainCount=1 这时候每当[object retain]一次retainCount+1(这里虽然简写也是rc不过是巧合或者当时开发人员故意选的retain这个词吧)每次[object release]时候retainCount-1 当retainCount==0时候object就真正把这快内存还给系统。

  1. 常用container的Reference Counting特性 这个规则很简单把。但是这块确实让新手最头疼的地方。问题出在,新手总想去验证rc规则,又总是发现和自己的期望不符合。 无数次看到有人写下如下句子

    NSLog(@”%d”,[object retainCount]);

    while([object retainCount]>0){ [object release]; }

当然了,我也做过类似的动作,那种希望一切尽在掌握中的心态。但是你会看到其他人告诉这么做完全没有意义,RC规则并不是这么用的。

首先,这个数字也许并不是你心目中的哪个。因为很难跟踪到底哪些地方引用的该资源。你建立的资源不光只有你的代码才会用到,你调用的各种Framework,Framework调用的Framework,都有可能改变这个资源的retainCount。

其次,这里每个数字意味着有其它对象引用该资源,这样的暴力释放很容易导致程序崩溃。就好比,其它人也许可以翻牌子把门口哪个牌子上的数字改变,但是这会出现问题。还有很多人在里面,把牌子变成0房间锁了结果谁也出不来。又或者,减少牌子上的数字,人进的过多房间变得过于拥挤。

所以去验证rc规则,或者单纯的改变retainCount并不是明智之举。你能做的就是理解规则,使用规则,读文档了解container的引用特性。或者干脆移到 Automatic Reference Counting (ARC) 上面。

我有一个NSMutableArray里面保存了1000个NSString对象,我在release的时候需要循环释放1000个string么?还是只需要release NSMutableArray。

就像上面提到的,如果你了解container的引用特性,这个问题自然就解决了。“NSMutableArray在添加、插入objects时会做retain操作。” 通过这一句话就分析出,用户不否需要帮助NSMutableArray释放1000个string。回忆上面提到的管理哲学,“谁分配谁释放” 编写NSMutableArray的程序员非常熟悉这个规则,NSMutableArray内部retain了,NSMutableArray自然要负责release。但是NSMutableArray才不会管你在外面什么地方引用了这1000个string,它只管理好内部的rc就够了。所以如果你在NSMutableArray外面对1000个string retain了,你自然需要release。相应的,你作为创建这个NSMutableArray的程序员,你只管release这个NSMutableArray就可以了。

最后说一下不用arc的情况。目前情况来看,有不少第三方的库并未支持arc,所以如果你的旧项目使用了这些库,请检查是否作者发布了新版本,或者你需要自己修正支持arc。

8. class heritage, category and extensions

Objective-C 的 OOP 特性提供 subclass 和 category 这2个非常重要的部分。subclass 应该反复被各种编程书籍介绍过。它是 OOP 继承特性的关键语法,它给类添加了延续并且多样化自己的方法。可以说没有继承就没有 OOP 这玩意。而 category 相对于 subclass 就不那么出名了。其实 category 思想出世于 smalltalk,所以它不能算是一个新生事物。

先说一下这2个特性最主要的区别。简单可以这么理解,subclass 体现了类的上下级关系,而 category 是类间的平级关系。

Subclass and Category

Subclass and Category

如上图所示,左侧是subclass,可以看到class, subclass1, subclass2是递进关系。同时下面的子类完全继承父类的方法,并且可以覆盖父类的方法。subclass2拥有function1,function2,function3三个函数方法。function1的执行代码来自subclass1, function2的执行代码来自于subclass2。

右侧是category。可以看到,无论如何扩展类的category,最终就只有一个类class。category可以说是类的不同方法的小集合,它把一个类的方法划分成不同的区块。请注意观察,每个category块内的方法名称都没有重复的。这正是category的重要要求。

经过上面简单解释了解了这2点的基本区别,现在深入说一下category。

在Objective-c语言设计之初一个主要的哲学观点就是尽量让一个程序员维护庞大的代码集。(对于庞大的项目‘原则’和‘协议’是非常重要的东西。甚至编写良好的文件名都是非常重要的开发技巧)根据结构化程序设计的经验出发,把一个大块代码划分成一些小块的代码更便于程序员管理。于是objc借用了smalltalk的categories概念。允许程序员把一系列功能相近的方法组织到一个单独的文件内,使得这些代码更容易识别。

更进一步的,和c,c++这种静态语言相比。objc把class categories功能集成到了run-time里面。因此,objc的categories允许程序员为已经存在的类添加新的方法而不需要重新编译旧的类。一旦一个category加入,它可以访问该类所有方法和实例变量,包括私有变量。

category不仅可以为原有class添加方法,而且如果category方法与类内某个方法具有同样的method signature,那么category里的方法将会替换类的原有方法。这是category的替换特性。利用这个特性,category还可以用来修复一些bugs。例如已经发布的Framework出现漏洞,如果不便于重新发布新版本,可以使用category替换特性修复漏洞。另外,由于category有run-time级别的集成度,所以使得cocoa程序安全性有所下降。许多黑客就是利用 category、posting2、Method Swizzling 等方法破解软件,或者为软件增加新功能。一个很典型的例子就是,我原来发布的QQ表情管理器(目前已经不再维护)。

值得注意的一点是,由于一个类的categories之间是平级关系。所以如果不同categories拥有相同的方法,这个调用结果是未知的:

Category methods should not override existing methods (class or instance). Two different categories implementing the same method results in undefined behavior.

(因为posting、Method Swizzling这个话题有些深入,本文里我就不介绍了。有兴趣自行Google)

Objc中Categories有其局限的部分,就是你不能为原有的class添加变量,只能添加方法。当然方法里可以添加局部变量。在这个局限基础上就有其它语言做了进一步改进,例如TOM语言就为Categories增加了添加类变量的能力。

自从Objc 2.0以后,语言引入了一个新的特性叫做 Class Extensions, 它可以看做是一类特殊的 category,可以给原有类增加新的属性和方法。

通过http://developer.apple.com/library/ios/documentation/cocoa/conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html介绍我们可以看出,如果 categories 是为类增加外部方法的话,那么 extensions 就是用做类的内部拓展。

Class extensions 的外观很简单,就是一个 Category 后面括号内的名字为空:

Objective-C
1
2
@interface ClassName ( )
@end

接下来,你就可以给你的类里添加属性,方法了:

Objective-C
1
2
3
4
5
6
@interface XYZPerson ( ) {
     id _someCustomInstanceVariable ;
}
@property NSObject *extraProperty ;
- ( void ) assignUniqueIdentifier ;
@end

Class extensions 常用来定义类的私有变量和方法。

总上所属,如果你开发时候遇到无论如何都需要为类添加变量的情况,最好的选择就是subclass。相反如果你只希望增加一些函数簇。Categories是最好的选择。而类内部需要用到的私有变量和方法则最好写在 Class extensions 里。

Categories关注的重心是代码设计,把不同功能的方法分离开。在Objc里因为Categories是runtimes级别的特性,所以这种分离不仅体现在源码结构上,同时体现在运行时过程中。这意味着一个category里的方法在程序运行中如果没有被调用,那么它就不会被加载到内存中。所以合理的使用categories会减少你的程序内存消耗。

所以我个人给大家的建议是,每个Cocoa程序员都应该收集整理自己的一套NS类函数的Categories扩展库。这对你今后程序开发效率和掌控情况都有很大提高。

9. Drawing Issues

大家知道,MacOS 是一个非常注重UI的系统。所以在 MacOS 编程里绘制是一个非常重要的部分。第9部分,我会介绍 MacOS 下绘制编程。

从绘制技术分类上看,Cocoa程序员能接触的几种绘制技术列表如下:

  1. Cocoa Drawing(NS-prefix)
  2. Core Graphics(CG-prefix, called Quazrtz 2D)
  3. Core Animation
  4. Core Image
  5. OpenGL

在这里我不打算给大家介绍如何绘制具体的按钮或者表格。只是介绍一下,它们的代码风格,优势和限制。

Cocoa Drawing

Cocoa Drawing应该是学习Cocoa程序开发最先接触的绘制技术。也是目前大多数MacOS程序所使用的绘制技术,其底层使用Quazrtz 2D(Core Graphics)。苹果对应文档为 Cocoa Drawing Guide。Cocoa Drawing并没有统一的绘制函数,所有绘制函数分散在几个主要的NS类的下面。例如, NSImage, NSBezierPath, NSString, NSAttributedString, NSColor, NSShadow,NSGradient …

所以很简单,当你看到如下代码就可以判断,使用的是Cocoa Drawing方法

Objective-C
1
2
3
4
5
6
7
[ anImage drawInRect :rect fromRect :NSZeroRect operation :NSCompositeSourceOver fraction : 1.0 ] ;
 
[ @"some text" drawAtPoint :NSZeroPoint withAttributes :attrs ] ;
 
NSBezierPath *p = [ NSBezierPath bezierPathWithRect :rect ] ;
[ [ NSColor redColor ] set ] ;
[ p fill ] ;

这种代码多出现在NSView的drawRect函数内。Cocoa Drawing 的渲染上下文是 NSGraphicsContext,我不断的看到很多新手把 NSGraphicsContext 和 CoreGraphics 的 CGContextRef 搞混。虽然它们很像并且也确实是有关系的,不过如果你不了解当绘制时候的 render context 很多时候将得到一个空白页面的结果。

Core Graphics

Core Graphics 是 Cocoa Drawing layer 的底层技术,在 iOS 开发中非常普遍,因为 iOS 系统中并不存在 Cocoa layer 所以网上可以找到的多是 Core Graphics 绘制代码段子,这给那些不了解 Mac 开发的新手来说造成了很大困扰。Cocoa 是 Mac OS 下的 application framework 而 iOS 下的 application framework 则是 UIKit.framework又叫 Cocoa Touch,它们分享部分代码基础但又不完全一样。例如,Cocoa Touch 下的 UIView 的渲染上下文会使用 UIGraphicsGetCurrentContext() 取得,它得到的是一个 CGContextRef 指针,而在 NSView 里多用 [NSGraphicsContext currentContext] 取得渲染上下文。它得到的是一个 NSGraphicsContext 对象。当然 NSView 里也可以通过 CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; 来取得一个 Core Graphics 渲染上下文。 可见 Mac OS 下的开发更为灵活一些。因为 iOS 中的 UIKit 开发初期就瞄准了显卡硬件加速,所有 UIView 都是默认 layer-backed 的。iOS 开发者必须使用 Core Graphics 和 Core Animation 这几个相对底层的绘制技术。

请看下面等价代码,作用是绘制一个白色矩形。但是分别使用 Core Graphics 和 Cocoa Drawing:

Objective-C
1
2
3
4
5
6
7
const CGFloat white [ ] = { 1.0 , 1.0 , 1.0 , 1.0 } ;
CGContextSetFillColor ( cgContextRef , white ) ;
CGContextSetBlendMode ( cgContextRef , kCGBlendModeNormal ) ;
CGContextFillRect ( cgContextRef , CGRectMake ( 0 , 0 , width , height ) ) ;
 
[ [ NSColor whiteColor ] set ] ;
NSRectFillUsingOperation ( NSMakeRect ( 0 , 0 , width , height ) , NSCompositeSourceOver ) ;

可以看出,这是2种风格完全不同的绘制技术。Cocoa Drawing 是分散式的绘制函数,而 Core Graphics 是传统的类似 OpenGL 的集成式的绘制方式。其实 Cocoa Drawing 下层是 Core Graphics, Core Graphics 的下层是 OpenGL。

在 OSX 下 NSGraphicsContext 和 CGContextRef 大部分时候是可以相互转换的。

NSGraphicsContext 到 CGContextRef:

Objective-C
1
2
3
4
CGContextRef ctx = [ [ NSGraphicsContext currentContext ] graphicsPort ] ;
CGContextSaveGState ( ctx ) ;
//Core Graphic drawing code here
CGContextRestoreGState ( ctx ) ;

CGContextRef 到 NSGraphicsContext:

Objective-C
1
2
3
4
5
NSGraphicsContext *ctx = [ NSGraphicsContext graphicsContextWithGraphicsPort :cgContextRef flipped :NO ] ;
[ NSGraphicsContext saveGraphicsState ] ;
[ NSGraphicsContext setCurrentContext :ctx ] ;
//cocoa drawing code here
[ NSGraphicsContext restoreGraphicsState ] ;

大部分时候使用 Cocoa Drawing 可以绘制出需要的效果,但是某些特殊时候需要 Core Graphic 绘制,例如一些特殊的阴影,clip效果,自定义pattern phase,blending style等等。

Core Animation

如果说 Core Graphics 和 Cocoa Drawing 是通用的 UI 绘制框架的话,那么 CA 显然是界面动画绘制的高级技术。 Core Animation 的对应 Cocoa Animation 部分应该是 NSAnimation 和 NSViewAnimation,但这2个差距比较大。NSAnimation 出现与 OS X 10.4,Core Animation 是 10.5 后出现的。NSViewAnimation 功能和使用相对简单。

简单来说,Core Animation 的作用对象是 CALayer, NSAnimation 的作用对象是 NSView。了解你的程序界面是在处理那种对象很重要。

Core Image

对于这个绘制技术,这篇文章给了我很多启示大家也可以看看。Notes on Rendering 2D Graphics on a Mac 虽然是一篇 note 但是,记录了很多实际应用中的经历,可以对个各种绘制技术有一个比较全面的解析。

根据此文的介绍。Core Image 适合处理小量大图,而非常不适合处理大量小图。因为 CI 利用 GPU 运算,而数据到 GPU 的round-trip 时间数量级在 millisecond。这就意味着,1000 张小图分别再 GPU 运算,时间至少再 1000*1 ms。此文作者尝试绘制3000张小图片,利用 Cocoa Drawing 原本耗时 750ms,但是改用CI后耗时猛增到3秒。

所以,这就是CI在osx绘制技术里所处的宏观角色:单图做实时处理。

openGL

待写…

10. design pattern

待写…

1:这里其实很有意思,为何我用“更高层次思考”,而不是“更底层次”。作为一个编译器和语言开发人员,面对的问题确实更底层没错,但是他们思考的维度更高,更抽象,这样子。一个不算恰当的比方就好像一个三维世界的人处理二维世界的一条线的问题。

2:Posting技术在10.5以后deprecated,并且64bit run-time也不再支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值