如何解决奔溃问题--SIGABRT和EXC_BAD_ACCESS

iOS:如何解决奔溃问题

程序遇到crash,不要惊慌;应该找到崩溃的地方—精确到文件、到哪行;而不是漫无目的的改代码,因为这样只会使情况更糟,出错程序:http://www.raywenderlich.com/downloads/Problems.zip
基本上有两种crash会发生:SIGABRT和EXC_BAD_ACCESS;SIGABRT是可以程序控制的崩溃,app会因为系统识别了app做了系统不支持的事情而终断;EXC_BAD_ACCESS是较难去排错的,因为它只出现在app处于崩溃的状态下,一般是由于内存管理的问题
第一步:为了找出确切位置在您的代码崩溃发生在哪个文件,哪一行。

(1)运行程序出错,类型为SIGABRT,这种错误类型会有错误信息,可以在xcode的Debug 输出面板中查看;要学会辨别错误信息,因为他们包含重要的出错的线索
此时线索是:[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840
在这里,有问题的对象是位于内存地址0x6a33840UINavigationController,方法是setList:知道奔溃的原因是好的,但你的行动首先当然是要弄清楚在那个代码中发生此错误。你需要找到源文件的名称和行为不端的代码所在行数。为此,您可以使用调用栈(又称堆栈跟踪或回溯)。
当一个应用程序崩溃的Xcode窗口的左窗格中切换到Debug导航。它显示是活跃在应用程序中的线程,并强调崩溃的线程。通常是线程1,应用程序的主线程,因为在那里,你会做你的大部分工作。如果您的代码使用队列或后台线程,那么应用程序可能会崩溃在其他线程。Xcode的突出问题的根源在main.m main()函数。这并不能告诉你非常多,所以你必须挖得更深一些。看到更多的调用堆栈,点击底部最左的列表图标

这里写图片描述

调用栈call stack显示了在app当前活跃的方法:start()调用main(),而main()调用UIApplicationMain().UIApplicationMain()调用UIApplication对象的_run方法,_run方法调用__pthread_kill.
这里写图片描述
所有这些函数和方法调用堆栈中,除了为main(),是灰色的。这是因为他们从内置在iOS框架。有没有为他们提供的源代码。
你源代码是唯一在这个堆栈跟踪main.m,所以这是什么Xcode的源代码编辑器中显示,尽管它不是一个真正的崩溃的真正来源。
(2)利用异常断点,找到出错的行

运行程序,找到出错的精确位置
application:didFinishLaunchingWithOptions: 方法的
viewController.list = [NSArray arrayWithObjects:@”One”, @“Two”];
再看看出错信息[UINavigationController setList:]: unrecognized selector sent to instance 0x6d4ed20;然后分析出错原因,并进行修正

注意:无论什么时候,运行得到 “unrecognized selector sent to instance XXX” ;注意核对调用方法的类名以及方法的名字,出错原因可能是,错误的对象调用了方法,也可能是拼写错误。

(3)再次运行程序,出现EXC_BAD_ACCESS
首先,按照系统给的警告,进行部分fit;
然后,可以看看call stack的信息,找到一些出错的线索,例如下面
这里写图片描述
因为此时异常断点还能用,所以点下面按钮来的到错误信息
这里写图片描述
注意:“this class is not key value coding-compliant for the key XXX” 错误的原因很可能是从nib文件中加载一个不存在的属性

第二部分
程序运行得到空的表

(1)使用NSLog(),同时不能忽略xcode的警告
预想的结果没有发生,几乎没有技术可以解决,但是可以使用NSLog()来处理。使用NSLog()来一步一步追踪程序的运行流程是否符合自己设定的流程

从这些方法的名字,我们可以猜测这个错误发生在重画这个tableview的某些地方。例如,我们可以看到layoutSubviews和_updateVisibleCellsNow:这些名字的方法
这里写图片描述
继续运行这个app来看看是否可以得到一些比较好的错误消息—–记住,现在只是在抛出异常的时候暂停了程序,并没有崩溃。点击继续程序按钮,或者在调试窗口键入下面的命令:

(lldb) c

你可能不得不多点击几次继续按钮,“c”命令也是一个简短的继续指令,和点击继续按钮一个效果,并不是就直接执行到最后。

现在这个调试窗口喷发出一些比较有用的信息:

***Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason:'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
***Firstthrow call stack:
(0x13ba0520x154bd0a0x1362a780x99a2db0xaaee30xab5890x96dfd0xa58510x50301
0x13bbe720x1d6492d0x1d6e8270x1cf4fa70x1cf6ea60x1cf65800x138e9ce0x1325670
0x12f14f60x12f0db40x12f0ccb0x12a38790x12a393e0x11a9b0x27220x2695)
terminate called throwing an exception

太好了,这是一个相当好的一个线索。显然这个UITableView的数据源没有从tableView:cellForRowAtIndexPath:方法返回一个有效的cell,因此在ListViewController.m方法里面增加一些调试输出信息来看看:

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
staticNSString*CellIdentifier=@"Cell";
UITableViewCell*cell =[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSLog(@"the cell is %@", cell);
cell.textLabel.text =[list objectAtIndex:indexPath.row];
return cell;
}

你增加一个NSLog()标记。再一次运行这个app,看看输出了什么:
Problems[18420:f803] the cell is(null)
从以上信息我们可以看出,调用dequeueReusableCellwithIdentifier:返回的却是nil,这就意味着使用“Cell”作为标识符的cell可能不存在(因为这个app使用的是标准的cell的storyboard)。
当然,这也是愚蠢的bug,因为xcode已经通过静态编译警告了你:“Prototype cells must have reuse identities。(标准的cell必须有重用的标识)”。这个是不能忽视的警告:
这里写图片描述
修改:打开storyboard,选择这个标准的cell(在tableview的顶端,并且显示的是“Title”的单独的一个cell),并且设置cell的标识符为“Cell”:将那个修复了之后,所以的编译警告应该没有了。运行这个app,现在这个调试窗口应该会打印出来:

Problems[7880:f803] the cell is<UITableViewCell:0x6a6d120; frame =(00;32044); text ='Title'; layer =<CALayer:0x6a6d240>>
Problems[7880:f803] the cell is<UITableViewCell:0x6877620; frame =(00;32044); text ='Title'; layer =<CALayer:0x6867140>>
Problems[7880:f803] the cell is<UITableViewCell:0x6da1e80; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9fae0>>
Problems[7880:f803] the cell is<UITableViewCell:0x6878c40; frame =(00;32044); text ='Title'; layer =<CALayer:0x6878f60>>
Problems[7880:f803] the cell is<UITableViewCell:0x6da10c0; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9f240>>
Problems[7880:f803] the cell is<UITableViewCell:0x6879640; frame =(00;32044); text ='Title'; layer =<CALayer:0x6878380>>

(2)改变你的NSLog()

6个table view cell被创建了,但是在table上面什么都看不见。怎么回事呢?假如你在模拟器里面到处点击一下,你将会注意到tableview中6个cell中的第一个却能够被选中。所以,显然cells都是存在的,只是他们都是空的:

将先前的NSLog()标记改变一下:

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
staticNSString*CellIdentifier=@"Cell";
UITableViewCell*cell =[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
cell.textLabel.text =[list objectAtIndex:indexPath.row];
NSLog(@"the text is %@",[list objectAtIndex:indexPath.row]);
return cell;
}

现在你打印出来就是你的数据模块的内容。运行这个app,看看显示出来的是什么:

Problems[7914:f803] the text is(null)
Problems[7914:f803] the text is(null)
Problems[7914:f803] the text is(null)
Problems[7914:f803] the text is(null)
Problems[7914:f803] the text is(null)
Problems[7914:f803] the text is(null)

上面的很好的解释了为什么在cell里面什么都没有看到的原因:因为这个文字(text)始终是nil。然而,假如你检查你的代码,并且在initWithStyle:方法里面显示的添加了很多的字符串到list array里面:

[list addObject:@"One"];
[list addObject:@"Two"];
[list addObject:@"Three"];
[list addObject:@"Four"];
[list addObject:@"Five"];

就像上面那样,这是测试你的假设是不是正确的一个很好的方法。可能你还想更准确的看看这个array里面到底有什么东西。改变先前在tableView:cellForRowAtIndexPath:里面的NSLog()为这样:

NSLog(@"array contents: %@", list);

至少这样可以给你展示一些东西。运行这个app。假如你还没准备好猜测会发生什么情况,调试窗口已经给你打印出来了:

Problems[7942:f803] array contents:(null)
Problems[7942:f803] array contents:(null)
Problems[7942:f803] array contents:(null)
Problems[7942:f803] array contents:(null)
Problems[7942:f803] array contents:(null)
Problems[7942:f803] array contents:(null)

哈哈,你的脸色瞬间阴沉下来。上面的代码居然没有起作用,因为你可能忘了在首先为这个array对象申请内存空间。这个“list”所以一直为nil,因此调用addObject: 和objectAtIndex:不会起任何的作用。

你应该在你的view controller被装载的时候为这个list对象分配空间,因此在initWithStyle:方法里面应该是一个不错的选择。修改那个方法为:

-(id)initWithStyle:(UITableViewStyle)style
{
if(self==[super initWithStyle:style])
{
list =[NSMutableArray arrayWithCapacity:10];
[list addObject:@"One"];
[list addObject:@"Two"];
[list addObject:@"Three"];
[list addObject:@"Four"];
[list addObject:@"Five"];
}
returnself;
}

试一试。我晕,依然什么都没有!调试窗口输出依然是:

Problems[7971:f803] array contents:(null)
...and so on ...

经过了这么多假设和修改,但是还是什么都没有,这些真的是非常令人沮丧啊,但是请记住你可能会一直继续到最后,直到你弄清楚了所有的假设。所以现在的问题就是难道initWithStyle:没有被调用?

(3)使用断点
你可能又会在代码里面放置其他NSLog()标志,但是其实你完全可以使用另一个工具:断点( breakpoints)。你已经看到过无论什么时候只要有异常抛出的时候,程序就会终止的异常断点(Exception Breakpoint)了。你其实也可以增加其他的断点,并且可以放置到代码的任何地方。一旦你的程序运行到断点的地方,这个断点就会被触发,并且程序就会进入调试模式。

你可以通过点击代码编辑区前面的行号来放置特殊的断点。行号前蓝色的箭头所指示的那一行就有一个断点了。你也可以在断点导航器(Breakpoint Navigator)里面看到这个新的断点:
这里写图片描述
再一次运行这个app。假如initWithStyle:确实是会被调用的话,那么你点击了“Tap Me!”按钮之后,当这个ListViewController被装载的时候,这个app将会暂停,并且会进入调试器。

可能正如你所料的,什么事情也没有发生。initWithStyle:没有被调用。其实这个是可以讲得通的,因为view controller是从storyboard(或者xib)中装载的,所以使用的应该是initWithCoder:方法。

将之前initWithStyle:方法替换为initWithCoder::

-(id)initWithCoder:(NSCoder*)aDecoder
{
if(self==[super initWithCoder:aDecoder])
{
list =[NSMutableArray arrayWithCapacity:10];
[list addObject:@"One"];
[list addObject:@"Two"];
[list addObject:@"Three"];
[list addObject:@"Four"];
[list addObject:@"Five"];
}
returnself;
}

并且保持断点在这个方法上面,来看看它是怎么工作的:

一旦你点击了那个按钮,这个app将会进入调试器:

以上的情况并不是意味着这个app崩溃了!它只是在这个断点处暂停了。在左边的执行堆栈里面(假如你没有看到执行堆栈的话,你可能需要切换到调试导航器),你可以看到你是从buttonTapped:到这里的。这个调试导航器里面,我们看到执行了一系列的UIKit的方法,并且装载了一个新的view controller。(顺便说句,断点是一个非常好的工具来指出这个系统是怎么工作的。)
这里写图片描述
如果想要离开你之前停留的地方,继续运行这个程序,简单的就是点击继续程序运行按钮,或者在调试控制台中输入“c”。

显然的是,一切并没有如我们料想的一样,这个app又奔溃了。我告诉过你,它有很多bug的。

注意:在你继续之前,在initWithCoder:移除断点或者使断点无效。因为他已经展现了他的目的,所以现在它可以离开了。

你可以在显示行号的的地方右击断点,并且在弹出的菜单中选择删除断点。你也可以拖出这个断点离开窗口,或者在断点调试器里面移除。

假如你并不想移除这个断点,你可以简单的使断点无效。为了达到这个目的,你可以使用右击弹出菜单,或者左击一次这个断点。判断这个断点是否有效,你可以看看这个断点的颜色,当为浅蓝色了就是无效了,深蓝色就是有效的。

(4)Zombies!

回到这个崩溃。它是一个EXC_BAD_ACCESS,调试器指到了他发生在tableView:cellForRowAtIndexPath:。你可以使用一个让你看到曙光的调试工具:Zombies!

打开这个项目的scheme editor:-》选择Run 选项,然后选择Diagnosics标签。勾上Enable Zombie Objects选项:

现在运行这个app。这个app仍然崩溃,但是现在你将会得到下面的错误消息:

Problems[18702:f803]***-[__NSArrayM objectAtIndex:]: message sent to deallocated instance 0x6d84980

上面这个就是zombie enable 工具所做的,做个小概括:无论什么时候你创建了一个新对象(通过发送“alloc”消息),一块内存将会为这个对象的实例变量保留。当这个对象被释放,他的保留计数(retain count)变成0,这块内存将会被释放。但是,你可能仍然有许多的指针指向这个已经失效的内存,这些都是建立在假设这里有一个有效的对象存在的情况下。假如你程序的某些部分试着使用这个野指针,这个app将会伴随着EXC_BAD_ACCESS的错误崩溃掉。

当这个zombie工具被启用之后,即使这个对象被释放了,这个对象的内存也不会被清理。所以,那块内存将会被标记为“长生不死的”。假如你试着之后又去使用这块内存,这个app能够意识到你的错误操作,并且app将会抛出“message sent to daellocated instance”错误并且终止运行。

因此这就是之前发生的事。这行就是使用了不死的对象:

cell.textLabel.text =[list objectAtIndex:indexPath.row];

这个cell对象和他的textLabel应该是好的,那么indexPath也应该是正确的,因此我猜测在这个问题下,这个不死的对象应该是“list”。

你多半其实已经有个很好的线索来怀疑这个“list”,因为这个错误消息说:

-[__NSArrayM objectAtIndex:]

这个不死的对象的类是__NSArrayM。假如你已经有一段时间的cocoa编程经验,你应该就会知道一些基本的类,就像NSString和NSArray实际上是“class clusters”,这就意味着就像NSString或者NSArray这些原始的类在一些底层的地方会被特殊的类代替。所以在这里你可以看到一些NSArray类型的对象,也就是这个“list”其实应该是一个NSMutableArray。

假如你却是想要确认一下,你可以增加一个NSLog()在分配了“list”数组那行代码之后:

NSLog(@"list is %p", list);

这里将会打印出和错误消息一样的内存地址(在我这里的情况下是0x6d84980,但是你自己测试的时候,地址就会不一样的)。

你也可以在调试器里面使用“p”的命令来打印出这个“list”变量的地址(和这个相对的命令就是“po”,这个命令将会打印出这个实际的对象,而不是地址)。这样方便的地方就是你可以省略很多额外增加NSLog()的步骤和从新编译这个app、

(lldb) p list

注意:非常不幸的是,上面这些命令在xcode4.3里面并没有执行的很好。由于一些原因,这个地址一直都是展示的0×00000001,可能是因为这个class cluster吧。在GDB调试器下面,那些命令就执行的很好,在调试器的变量窗口展示出“list”都是zombie。因此我觉得这个是LLDB的bug。
这里写图片描述
为这个list 数组分配空间的地方就在initWithCoder:,就是下面这样:

list =[NSMutableArray arrayWithCapacity:10];

由于这里不是ARC(Automatic Reference Counting)(自动引用计数)项目,所以是人工管理内存,所以这里你需要retain这个变量:

// in initWithCoder:
list =[[NSMutableArray arrayWithCapacity:10] retain];

为了避免内存泄露,你也不得不在dealloc函数中释放这个对象,就像下面这个:

-(void)dealloc
{
[list release];
[super dealloc];
}

再一次运行这个app。它又崩溃在这同样的一行,但是注意这个调试窗口输出的东西改变了:

Problems[8266:f803] array contents:(
One,
Two,
Three,
Four,
Five
)

由上面信息可以知道这个array已经分配了内存空间和包含了字符串的。这个崩溃的提示不再是EXC_BAD_ACCESS,而是SIGABRT,所以你需要再一次设置这个Exception Breakpoint。将这个解决了,继续找其他的bug!

注意:即使你使用了ARC,在这样的内存管理错误下也是一个非常大的事,你也会崩溃,得到一个EXC_BAD_ACCESS的错误,特别是假如你使用了不安全保留属性。

我的小提议:无论你什么时候得到一个EXC_BAD_ACCESS错误,你都可以开启zombie objects,然后再试试。

注意一点:你不应该一直启用zombie objects。因为这个工具将永远不会释放内存,只是简单标记一下这个内存是不死的,你最终将会在某个时候耗尽所有的内存。因此你应该在排查内存相关的错误的时候才开启zombie objects,其他时候应该关闭它。

(5)单步调试
重新运行这个程序,点击按钮。你将会在第一次执行tableView:cellForRowAtIndexPath:的时候进入调试器。注意啊,这个时候,app只是因为断点暂停了,并没有崩溃。

你想要准确的知道这个程序崩溃时的一些细节。请点击继续执行按钮,或者在(lldb)的提示后输入“c”来继续执行。程序将会从暂停的地方继续执行。

什么事情也没有发生,你仍然暂停在tableView:cellForRowAtIndexPath:这个函数的断点处。但是在调试窗口却显示:

Problems[12540:f803] array contents:(
One,
Two,
Three,
Four,
Five
)

这就意味着tableView:cellForRowAtIndexPath:在第一次执行的时候没有任何问题,因为NSLog()在断点之后执行了。因此这个app能够很好地创建第一个cell。

假如你键入以下的到调试提示之后:

(lldb) po indexPath

在调试窗口应该可以输出下面的:

(NSIndexPath*) $3 =0x06895680<NSIndexPath0x6895680>2 indexes [0,1]

以上重要的部分是[0, 1]。就是这个NSIndexPath对象为section 0和row 1。换句话说,这个tableview现在就在请求第二行。从这里我们可以推测这个app在第一次创建cell的时候没有任何问题,正如刚刚这里就没有发生崩溃。

多点几次这个继续按钮。在某一个特定的时候,这个程序崩溃了,并且输出一下错误消息:

Problems[12540:f803]***Terminating app due to uncaught exception 'NSRangeException',
reason:'*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 4]'
***Firstthrow call stack:
...and so on ...

假如你检查这个indexpath对象的话,你可以看到:

(lldb) po indexPath
(NSIndexPath*) $11 =0x06a8a6c0<NSIndexPath0x6a8a6c0>2 indexes [0,5]

Section依然是0,但是这个row的索引是5。注意哦,这个错误的消息也是说“index 5”。因为计数是从0开始的,当到5的时候实际上意味着已经是6的位置了。但是这里只有5项。显然这个tableview认为这里实际上有更多的行。

所以这个犯人就是下面的方法:

-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
return6;
}

这个方法其实应该被写成这样的:

-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
return[list count];
}

删除断点或者使断点无效,然后再次运行这个程序。终于这个tableview显示出来了,并且没有了崩溃!

注意:这个“po”命令对于检查你的对象是非常有用的。你可以在程序暂停在调试器的时候,或者在设置一个断点的时候,或者在崩溃的时候,使用这个命令。你需要确定的是这个方法当前在调用堆栈里面是高亮的,否则这个调试器将找不到这个变量。
你也可以在调试窗口的左边看到这些变量,但是就算看到了也不是很方便就能知道细节的:
我刚刚说了没有崩溃的现象了?

这里写图片描述

好,现在我们来试试滑动删除。这个app又终止了在tableView:commitEditingStyle:forRowAtIndexPath:

错误消息是:

Problems[18835:f803]***Assertion failure in-[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:1046

这个错误看起来像是来自UIKit,并不是来自app的代码。多次输入几次“c”来让系统抛出异常,这样可以你可以得到更多有用的信息:

***Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason:'Invalid update: invalid number of rows in section 0. The number of rows
contained in an existing section after the update (5) must be equal to the number
of rows contained in that section before the update (5), plus or minus the number
of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or
minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
***Firstthrow call stack:...

上面的意思是这个app告诉这个tableview里面一行要删除,但是却忘记从数据源里面移除这行的数据。因此这个table view看起来没有什么改变。修改这个这方法:

-(void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath
{
if(editingStyle ==UITableViewCellEditingStyleDelete)
{
[list removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}

太好了,看起来这样做起效了,你终于有一个不会崩溃的app了。

记住下面几点:

假如你的app崩溃了,第一件事就是找到是哪里崩溃了,为什么崩溃了。一旦你知道了这两点,修复这个崩溃就很简单了。调试器可以帮助你,但是你需要知道怎么样让他帮助你。

有些崩溃可能是随机出现的,这个也是最困难的一个,特别是当你正在使用多线程。但是大多数,你可以试试,会发现一些固定的方法来让你的程序每次崩溃。

你可以想出怎么使用最少的步骤来减少崩溃的现象,这样你将找到一个好的方法来修复这个bug(也就是说他将不会发生)。但是假如你没有确定不会再生了这个错误,你就绝不能确定你的修改已经修复了这个bug。

秘诀:

1.假如崩溃在main.m里面,就可以设置全局异常断点(Exception Breakpoint)。

2.在异常断点开启的状态下,你也没有得到得到有用的信息。在这种情况下,多继续几次运行这个app,或者在调试提示后面输入“po $eax”命令。

3.大多数崩溃的一般原因和一些bug都是在你的xib中或者storyboard中的连接丢失了或者是错误的连接。这些情况不会在编译错误里面显示,因此你一般不知道。

4.不要忽略编译警告。假如你有编译警告,就说明你有些东西可能会出错。假如你不知道为什么你会到一个编译警告,最好去搞明白它. 这些都是安全的做法!

5.在设备上调试可能会和在模拟器上面有些微的不同。这两个环境不是完全一样,你将会得到不同的结果。

例如,当你运行一个有问题的程序在iphone4上的时候,这第一个崩溃就会发生在NSArray初始化的时候,因为你缺少一个nil标记,而不是会因为当这个app执行setList:的时候的时候崩溃。所以说上面那个原则方法就可以帮你找到崩溃问题的根源本质。

不要忘记静态分析工具(static analyzer tool),这个工具将会捕获更多的错误。假如你是一个初学者,推荐你开启它。你可以在Build Settings界面上为你的工程设置:

参与评论 您还未登录,请先 登录 后发表或查看评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:终极编程指南 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值