Intermediate Debugging with Xcode 4.5

The one single constant in software development is bugs. Let’s face it, we don’t always get it right the first time. From fat fingers to incorrect assumptions, software development is like baking cakes in a roach motel – except we supply the critters!

Luckily, Xcode gives us a myriad of tools to keep the nasties at bay. There’s obviously the debugger we know and love, but there’s a lot more it can do for you than just examine variables and step over code!

This is a tutorial for intermediate iOS developers, where you’ll get hands-on experience with some of the lesser known but extremely useful debugging techniques, such as:

  • Getting rid of NSLog in favor of breakpoint logging
  • Getting rid of comment TODOs in favor of compiler warnings
  • Breaking on conditions with expressions
  • Dynamically modifying data with LLDB
  • And much more!

You see, for me the goal is to be a lazy developer. I’d rather do the heavy work up front so I can relax on the backend. Thankfully, LLDB (Xcode’s integrated debugger) values my martini time. It provides great tools so I don’t have to be glued to my computer all day and night.

Let’s take a look at these tools. Pull up a bean bag chair. Crack open your favorite beverage. It is time to get lazy! :]

Note that this tutorial assumes you already know the basics about using the Xcode debugger. If you are completely new to debugging with Xcode, check out this beginner debugging tutorial first.

Getting Starting

I put together a sample app for this project. You can download it here.

The app is called Gift Lister. It tracks gifts you might want to buy for people. It’s like Gifts 2 HD which recently was awarded Most Visually Impressive by this site. Gift Lister is like Gifts 2 HD but far far worse.

For one thing, it’s filled with bugs. The developer (myself in a different shirt) was being ambitious and tried to fix the app the old fashioned way. Yes, it’s still broken :]

This tutorial will walk you through the steps on how to fix the app while being as lazy as possible.

Okay, let’s get started, but don’t feel the need to rush :]

Open up the project and take a look around the various files. You’ll notice that the app is a simple front end to a basic Core Data backing store.

Note: If you do not know Core Data, don’t worry! Core Data is an object persistence framework which is awhole tutoria to itself. In this tutorial, you will not diving into the framework nor will you be interacting with Core Data objects in any meaningful way, so you don’t need to know much about it. Just keep in mind that Core Data loads objects and saves them so that you don’t have to.

Now that you’ve taken a look around, build and run the app.

Not surprisingly, the app crashed. Let’s fix it up.

Setting up the Debugger Console

The first thing to do whenever you start a debugging session is to open the debugging console. You can open it by clicking this button on the main toolbar:

While the button is nice and convenient, clicking it each and every debug session provides unnecessary wear and tear on your fingertip. :] This is why I prefer to let Xcode do it for me.

To do so, open Xcode preferences by pressing ⌘, or by going to the application menu and selectingXcode\Preferences. Click the Behaviors button (the button with the gear over it).

Behaviors Dialog

Click the ‘Starts‘ item on the left hand side of the dialog. You will see a bunch of options appear on the right hand side. On the right hand side, click the seventh checkbox and then select ‘Variables & Console‘ on the last dropdown.

Do this for both ‘Pauses‘ and ‘Generates Output‘ items which are located just underneath the ‘Starts’ item.

The ‘Variables & Console’ option tells the debugger to show the list of local variables as well as the console output each time a debugger session starts. If you wanted to view just the console output, you would select ‘Console View’. Likewise, if you wanted to see just the variables, you would select the ‘Variable View’.

The ‘Current Views’ option defaults to the last debugger view on your last debugger session. For example, if you closed Variables and opted to just the view the console, then just the console would open next time the debugger was started.

Close out the dialog, then build and run.

The debugger will now open each time you build and run your app – without you having to go through the major bother of clicking that button ;] Although it only takes a second to do that, it adds up. And after all you’re trying to be lazy! :]

The NSLog Jam

Before continuing, it is important to review the definition of a breakpoint.

A breakpoint is a point of time in a program that allows you to perform actions on the running program. Sometimes, the program may pause at the designated point to allow you to inspect the program’s state and/or step through the code.

You can also run code, change variables, and even have the computer quote Shakespeare. You will be doing all these things later in the tutorial.

Note: This tutorial will be covering some of the advanced uses of breakpoints. If you are still wrapping your head around some of its basic uses such as stepping-in, stepping-out, and stepping-over, please read over the My App Crashed, Now What? tutorial.

Okay, now build and run the app. This is the result of your first attempt at running this app:

Can feel you the hours ticking away?

This project needs a little sanity. Currently, you cannot see the source of the compile error. To find it, you need to add an exception breakpoint to track down the source of the error.

So switch to the breakpoint navigator as shown below:

Then, click the plus sign at the bottom of the pane. From the menu, select Add Exception Breakpoint.

You should now see this dialog:

Exception Breakpoint

The Exception field gives you the option of activating the breakpoint in Objective-C, C++, or All. Keep the default option of All.

The Break field in the dropdown allows you to pause execution on whether an error is thrown or caught. Keep it selected on thrown. If you are actually making use of exception handling in your code, then select ‘On Catch’. For the purposes of this tutorial, leave it ‘On Throw’.

We will discuss the final two fields later in this tutorial. Click the Done button and then, build and run.

This time the result is much cleaner.

Take a look at the debugger console It is filled with log messages. A lot of them appear unnecessary.

Logging is critical to debugging code. Log messages need to be actively pruned else the console becomes littered with “noise”. Sifting through all that noise takes away from time on the golfing range, so it’s important that it is removed, otherwise you’ll be wasting more time on a problem than it deserves.

Open the AppDelegate.m and you should see a bunch of old messages in didFinishLaunchingWithOptions. Select them all and delete them.

Let’s find the next set of log statements. Open up the search navigator, and look for in NSLog(@”in viewDidLoad”);

Click the search results and FriendSelectionViewController.m will open to the line with log statement.

At this point, the effort you are putting into managing your log statements is starting to accumulate. It may not seem like a lot, but every minute does add up. By the end of a project cycle, those stray minutes can easily equate to hours.

The other disadvantage of hard coding your log statements is that each time you add one to the code base, you take a risk of injecting new bugs into your code. All it takes are a few keystrokes, a little autocomplete, then a small distraction – and your once working app now has a bug.

It’s time to move those log statements out of the code to where they belong. Breakpoints.

First, comment out both of the logging statements. Next, add a breakpoint by left clicking it in the gutter besides each of the statements.

Your code window should look like this:

Control click or right click the first breakpoint and select ‘Edit Breakpoint’. From the dialog, select ‘Log Message’ from the Action dropdown. In the first text field, type ‘in viewDidLoad’. The dialog should now look like the following:

Click the done button, then build and run. You should now see ‘in viewDidLoad’ in the console – but now it’s done with breakpoints instead of NSLog statements!

Note: Througout this tutorial, you will be clicking build and run after each breakpoint modification as its quicker to explain. The key point to remember: breakpoints are a runtime addition. You can add as many of them as you want during the execution of your program. This includes NSLog statements.

There is one major problem. The program is stopping at that breakpoint when you want it to continue. Changing that behavior is simple.

Control click or right click the breakpoint and select ‘Edit Breakpoint’. At the bottom of the dialog, click the ‘Automatically continue after evaluating’ checkbox. Now build and run again.

This time it correctly logs the message, only it pauses on the second breakpoint.

Control click or right click the second breakpoint. Select ‘Log Message’ in the action dropdown, then type “Loading friends…”. At the bottom of the dialog, click the ‘Automatically continue after evaluating’ checkbox. Click done. Now build and run again.

The app works great until it crashes. You can’t have everything :]

Believe it or not, you’re still doing too much work. Control click or right click the first breakpoint and replace “in viewDidLoad” with %B. Now run the app again. The console should look like this:

The %B prints out the name of the containing method. You can also use the %H to print out the number of times the method is being touched. Simple expressions can also be included.

So you could write: %B has been touch %H times. The console will read: -viewWillLoad has been touched 1 times.

Before you actually fix the crashing bug, let’s have one last bit of fun. Control click or right click the first breakpoint. Click ‘Edit Breakpoint” from the menu. In the dialog, click the plus button. This button allows you to add multiple actions to a single breakpoint.

Select the “Log Message” action only this time, type “To be, or not to be”. Press the “Speak Message” radio button, then click done. The dialog should look like this:

Now build and run and enjoy the performance.

Note: Novelty aside, this feature can actually be useful! Audio messages can be especially useful when debugging complicated networking code and the like.

Unfortunately, besides printing out simple string messages, the Log Messages action doesn’t have the same flexibility of NSLog. For that, you really need to add some Debugger Actions.

To demonstrate, you’ll fix the crashing bug. Build and run and then let the program crash. The stack trace reads:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name 'Friend'

Something is not working in Core Data.

Scanning the code, you see that the NSManagedObjectContext is being pulled from the DataStore object. You have a hunch that the DataStore is probably the cause of the problem. The DataStore is not part of Core Data. It is a custom singleton object used to encapsulate some of Core Data’s central objects.

Add a breakpoint underneath this line: DataStore *dataStore = [DataStore sharedDataStore];

Control click or right click the breakpoint, click ‘Edit Breakpoint’, then select select “Debugger Command” from the dropdown. In the text field type the follow:

po dataStore

Click the “Automatically continue after evaluating” checkbox and build and run.

As you suspected, the dataStore is nil.

Note: po is a debugger command that will print out the contents of an object. If you need to print out the contents of a primitive, use p instead. There are a lot of debugger commands that are beyond the scope of this tutorial. To learn more of them, head over to the LLDB documentation.

Open DataStore.m and you’ll see in the sharedInstance method that a nil value is being returned. Change the return value from

return nil

to

return sharedInstance

Build and run. Hooray, the app is (kind of) working!

Gift Lister - now running!

Breakpoints and Expressions

So far so good, but you may have noticed that the breakpoint logging doesn’t show a timestamp of when the log message occurs, which can sometimes be useful for debugging purposes. The good news is it’s easy to fix with breakpoint expressions!

Note: Date logging is indeed useful, but keep in mind it also makes the logging a bit slower as it has to query all the date information. Keep that in mind if you find your logging calls lagging behind your application.

Let’s restore your log statements to their previous glory. Right click or control click the previous breakpoint inFriendSelectionViewController.m. Click ‘Edit Breakpoint’. In the debugger action, change the command to read:

expr (void)NSLog(@"dataStore: %@", dataStore)

The expr command will evaluate an expression in real time. The expression command needs to know the actual type of the returning value, so a cast is necessary. Since there is no return type for NSLog, the return type is cast to a void value. Build and run.

You should now see the following:

2012-12-20 08:57:39.942 GiftLister[1984:11603] dataStore: <DataStore: 0x74c3170>

Being able to add NSLog messages via. breakpoints means you no longer have to stop the program just to log important data, there’s no chance of introducing new bugs because you are not touching the code, but best of all, there’s no last minute scrambles to remove all your debug statements the night before release.

There is one small difference between calling NSLog in the debugger versus calling it in the code. Underneath the actual log message, the text “<no result>” is printed. This is generated from LLDB and unfortunately, you cannot suppress this message. The good news is that this message should be going away in Xcode’s next point release.

Now let’s disable logging in the app. It’s just a matter of pressing the breakpoints button.

Click it and then build and run. Your logs are clean.

You can also individually turn off log calls by heading over to the breakpoint navigator.

The days of filling your codebase with commented out log calls are now over! :]

Warnings, Errors, Return Values, oh my!

The app is running. The next thing to do is to create some friends so you can keep a list of gift suggestions for them.

Build and run the app, and when the app starts, press the table cell which says, “Add a friend”. The app loads another view controller with a name text field and a date picker. Enter a name and select a birthday. Press the OK button.

You’ll be returned back to the root controller with your new friend added to the table. Click the “Add a friend” cell once again.

Enter the name of another friend, only this time, for the birthday, select February 31st, 2010.

In a typical date picker such a date would not be pickable. This is not the case with this app. In a fit of delirium, I decided to be ambitious by choosing the regular picker instead of the date picker. Doing so, I was forced to rewrite all of the date validation logic which, of course, created some new bugs.

Press the OK button. Tragically, the invalid date is recorded. It’s time to do a little debugging to see what is wrong.

Open the AddFriendViewController.m and add a breakpoint at the start of the method -(void)saveFriend.

Note: Finding methods in large files can take a lot of time. The long way is to manually scan each line until you stumble into it. The second way is to use the jump bar, then scroll through the list of method names. I actually prefer to do a search, although not in the search navigator, but in the jump bar itself. To do so, click on the jump bar then just start typing. Your method name should show up like it were in a regular search field.

In the simulator, press the “Add a friend” button and like your previous entry, add an invalid date. Step down the method until you reach this line:

if ([self isValidDateComposedOfMonth:month day:day andYear:year]) {

Step into method. The validation code failure is clear. There isn’t any. There’s just a comment promising to do it sometime in the future.

Comments are nice way to describe the particular meaning of code chunks, but using them for task management is futile. Even on tiny projects, there’s just too many items to juggle that these comment tasks will be forgotten.

The best way to prevent them from being lost is to really make them stand out.

In the first line of the method -(void)isValidDateComposedOfMonth, write the following line of code:

#warning add validation code

Right away, the project will report a new warning. Click on the Issue Navigator and you’ll see the warning listed, along with your message.

If you are the type of developer who ignores warnings, try out this variation of it:

#error fix your code

Right away, the new error is reported. You will be unable to compile your code until that line is deleted. That’s one way to keep track of your comments :]

Delete both the messages so the app will compile.

You can also leave messages in the jump bar. In the first line of the method -(void)isValidDateComposedOfMonth, write the following line of code:

// TODO: Add validation code

Save the file, then open the jump bar. You should see something like this:

You can also write: FIXME:, ???:, and !!!:. The ???: means, “I have a question for you” whereas the !!!: means, “this is important”.

These statements don’t have the same emphasis as the compiler warning or error, but they at least have a greater visibility than a lone comment at the bottom of a method. It’s best to leave comments for, well, comments and keep a list of required tasks outside of the codebase.

Now let’s investigate a nice little feature that was included in Xcode 4.4.

Restart the app, keeping the breakpoint fixed in the empty validation method. Now, step out of the code. Look at the Variables view in the debugger. You should see this:

This is a feature that hasn’t received much attention, but it makes your life so much easier. Consider that the code was being called from here:

if ([self isValidDateComposedOfMonth:month day:day andYear:year]) {

The code that is calling the method then immediately using the return value in an expression. Prior to this being added in Xcode, if you wanted to inspect return values, you needed to break apart the line, then log out the value.

Now, you can just step out of a method and see the return value right in the debugger.

Sounding Out Your Save Methods

At this point, you should have plenty of data in the app. It’s time to save it all. With apps like this, saving should be done frequently so that nothing is lost. That’s not the case with this app. It only saves when the user exits the application.

Click Back on the navbar to return to the root view controller, then simulate a home button press. You can do this from the Simulator’s menu by selecting Hardware\Home or by pressing shift-command-h.

Now stop the program from Xcode, and build and run. The tableview is empty. The app failed to save anything.

Open AppDelegate.m. In the applicationDidEnterBackground method, you should see the problem at once. There is a method called doLotsOfWork. The work isn’t being finished in time, so iOS is terminating your app before it finishes its cleanup. The result of this early termination is that the saveData method is not being called.

Let’s make sure that data is saved first. In the applicationDidEnterBackground, move the [[DataStore sharedDataStore] saveData]; method above the doLotsOfWork method like so:

[[DataStore sharedDataStore] saveData];
[self doLotsOfWork];

Now, add a breakpoint on the doLotsOfWork line. Right click or control click the breakpoint and select ‘Edit Breakpoint’. Select a sound action and choose “Submarine” as the sound. When dealing with sound actions, I try to avoid system sounds as I may easily overlook them.

Next, click the checkbox next to “Automatically continue after evaluating”. Finally, click build and run.

When the app starts again, add a new user then press the home button in the simulator. Just after the app closes, you should hear the submarine sound, indicating that the data has been saved.

Stop the app in Xcode, then press Run. You should see the data in all its glory.

Playing a sound is a good way to know if a certain code path has been reached without having to look through the logs. You can also provide your own custom sounds in case you want to play an explosion for a particular bad crash.

To do so, just drop your sound files in this folder:

YOUR_HOME_DIRECTORY/Library/Sounds

You’ll have to restart Xcode before you can use them, but think of all the potential shenanigans :]

Conditions for Successful Debugging

There are times in development when it is necessary to change the state of your program at certain intervals. Sometimes these changes occur in the middle of large sequences of events which makes normal debugging quite difficult. That’s where conditions come into play.

Now that you have some friends listed in the app, tap one of their names in the root view controller to bring up the gift interface. It’s just a simple grouped table that can be sorted on whether the gift can be purchased or not.

Press the add button on the navigation bar to add a new item. For the name, put shoes. For the price, put 88.00. Tap the OK button. The shoes should now appear on in the gift table.

Now add the following items:

Sleigh / 540.00
Candles / 1.99
XBox / 299.99
iPad / 499.99

Yikes. You realized that you actually wanted to record a PS3 instead of an XBox. You could simply tap the cell to edit it, but for the sake of demonstration, you will edit it through the debugger.

Open up GiftListsViewController.m and look for the method cellForRowAtIndexPath. Add a breakpoint on the line underneath the code that reads:

if (gift) {

Now right click or control click the breakpoint, and select ‘Edit Breakpoint’.

It’s time to put your condition. Think of this like a simple if statement. Add the following code:

(BOOL) [gift.name isEqualToString:@"XBox"]

LLDB requires us to provide a cast which is why you have the BOOL before the expression. Click done.

Now, press the Bought segmented control button. The table reloads new data but the breakpoint does not trip.

Press the Saved segmented control button. This time everything should pause with the highlighted item selected in the debugger console.

In the debugger console, add the following:

(lldb) expr (void) [gift setName:@"PS3"]

Now, press the play button and the table will continue to load. The PS3 replaces the XBox in the gift results.

This same results can accomplished by setting the number of iterations. Control click or right click the break point, and select ‘Delete Breakpoint’. Xcode can get a little wonky when editing conditions. It is best to start with a clean slate. Add a new breakpoint in the same place. This time, in the ignore text field, select the number 3. Click done.

Now press the Bought segmented control then the Saved segmented control.

We should hit the same breakpoint. To confirm that you are at the correct object, type:

(lldb) po gift

Now revert the object back to its previous state:

(lldb) (void)[gift setName:@"XBox 360"]

The table should now reflect the update. Isn’t real time editing just great?

Starting up by Tearing Down

When developing data driven apps, often times, it is important to wipe the data store clean. There are a number of ways of doing this from reseting the iPhone simulator to locating the actual datastore on your computer and deleting it. Doing this over and over can be a bit tedious, so get a little lazy and have Xcode do it for us.

We’ll start by creating a shell script. A shell script is a list of commands that automate some actions of the operating system. To create a shell script, create a new file from the application menu. Click File\New\File or command-n. From the category listings, select Other and then select Shell Script as the type.

For the name, put wipe-db.sh

Now you have to find the actual data store. Open up the terminal is located. If you don’t know where the terminal is located, you can find it in your Application folder inside of the Utilities folder.

Once the terminal has started, change your location to your home directory by entering the following:

 YourComputer$ cd ~

Next list out the directory contents by entering:

 YourComputer$ ls

Give the directory listing a good look over. If you do not see a folder labeled Library, enter the following command:

 YourComputer$ chflags nohidden ~/Library/

Then restart the terminal.

Now, move to the folder containing the iPhone simulator by entering the command:

 YourComputer$ cd ~/Library/Application\ Support/iPhone\ Simulator/6.0/Applications

List out the directories by typing:

 YourComputer$ ls

You may see a lot of different directories, depending on how many apps you currently have installed in simulator. You need to find the folder for GiftLister so it may take some trial and error. To enter a folder, you will need type: cd THE_NAME_OF_YOUR_FOLDER

To save some time, only type in the first three letters of the folder’s name, then press tab. The terminal will autocomplete the folder for you. If not, keep typing letters until it does. In my case, I have the folder: 0B1E5AD3-7292-45A6-BB5D-F1C004AC47F9 so I would type

 YourComputer$ cd 0B1

Then I would press tab.

Once inside you should see the file: GiftLister.app. If not, try another folder. Also, this project is based on iOS 6. If you are using an earlier simulator, then enter the following:

 YourComputer$ cd ~/Library/Application\ Support/iPhone\ Simulator/

Then type ls to display the contents. Choose the correct simulator version, and enter the directory by typing: cd VERSION_NUMBER/Applications (for example, cd 6.0/Applications

Once you have found your application folder, enter the following:

 YourComputer$ cd Library

Now list the directory contents by typing:

 YourComputer$ ls

You should see the file giftlister.sqlite. Jackpot.

Now print out the path to the file by typing the following:

 YourComputer$ pwd

Copy the full path of the file and paste it into the shell script. At the end of the path, add the following:

/giftlister.sqlite

Your full path should look something like this:

/Users/Brian/Library/Application Support/iPhone Simulator/6.0/Applications/0B1E5AD3-7292-45A6-BB5D-F1C004AC47F9/Library/giftlister.sqlite

Unfortunately, you can’t have spaces in the path so you need to transform these words:

/iPhone Simulator/
/Application Support/

to look like this:

/iPhone\ Simulator/
/Application\ Support/

The completed path should look like the following:

/Users/Brian/Library/Application\ Support/iPhone\ Simulator/6.0/Applications/0B1E5AD3-7292-45A6-BB5D-F1C004AC47F9/Library/giftlister.sqlite

Preceding the path, add the remove command which is simply rm. The line should look like:

 YourComputer$ ls

Here’s what the completed shell script will look like:

Save the shell script and close it.

By default, shell scripts are read only. you need to make this one executable. With the terminal open, return to your home directory by entering the following:

 YourComputer$ cd ~

Now, list the contents of the directory by typing:

 YourComputer$ ls

You will have to navigate to the location of your project folder. If you placed it on your desktop, you would navigate to it by typing:

 YourComputer$ cd Desktop
 YourComputer$ cd GiftLister

If you have to navigate up a directory, type the following:

 YourComputer$ cd ..

After a long crawl through the terminal, you should see all the project files. To make the shell script executable, type the following:

 YourComputer$ chmod a+x wipe-db.sh

Chmod is a program that changes the permissions of a file. The a+x allows the file to be executable for all users, groups, and others.

Wow … that was a lot. Take a breather. You deserve it. Sometime being lazy can take a lot of work.

Close the terminal and return to Xcode. Open AppDelegate.m. Set a breakpoint on the first line of the method didFinishLaunchingWithOptions. Right click or control click the breakpoint and select ‘Edit Breakpoint’. Add an action and select ‘Shell Command’. In the next dialog, press the Choose button and select the shell script that you just created. Click the “Automatically continue after evaluating” checkbox, then press done.

Stop the simulator if it is running. Now build and run. The database is now deleted.

The simulator tends to cache a lot of data, so I find the best thing to do is a clean build by pressing Clean from Xcode’s product menu, then build and run. Otherwise, you can run the app, stop it, then run it again. The cached data will be gone with a brand spanking new database.

While it did take some bit of work to setup, now clearing out the database can be preformed with the press of a button. When not in use, simply disable the breakpoint.

Note: You just created a shell script and wrote a simple Unix command to delete the file. You can just as easily have loaded a PHP file within the shell script to do the same thing. You could also launch a Java program, python script, or any other program on the machine. The key point, you don’t need to learn shell scripting to manipulate the underlying operating system through a breakpoint.

Where to Go from Here?

As you can see, the debugging tools have a lot of flexibility in meeting many of the challenges that you face day to day in your development process. This provides us the ability to dynamically inspect and update your code without having to worry about injecting any additional bugs.

Believe it or not, this is just the beginning. LLDB provides a host of other features such as custom variable summaries, dynamic breakpoints, and even custom scripting of the debugger through Python.

Granted, moving beyond NSLog() debugging can be challenging, but at the end of the day, you’ll find your projects will be stronger for it. You will no longer have to worry about stripping all your debugging code on the eve of launch nor will you have to write complex macros to provide a sane debugging environment. Xcode provides you all the tools so you can relax at the end of day :]

If you want to learn more, LLDB is a good place to start. Also, check out the WWDC 2012 debugging sessions. If you have any questions, please let me know in the forum discussion below!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值