Apple's "one size fits all" strategy makes its products more and more like a hard pill to swallow. Although Apple has brought some workflows to iOS/OS X developers, we still hope to make Xcode more convenient through plugins! Although Apple does not provide any official documentation to guide us on how to create an Xcode plugin, the developer community has done a lot of work to develop some very useful tools that can be used to help developers. From plugins that autocomplete image names, to plugins that clear cache, to plugins that turn Xcode into a vim editor, Xcode's plugin community has expanded our thinking and we can make Xcode smarter. In this not-so-short three-part tutorial, you'll create an Xcode plugin to entertain your coworkers by showing them something they don't expect to see! Even though this plugin is lightweight, you'll still learn a lot, such as how to debug Xcode, find the element you're interested in and modify it, and how to replace system functions with your own (via swizzling)! You will use x86 assembly knowledge, code definition skills, and LLDB debugging skills to view undisclosed private frameworks, explore private APIs in these private frameworks, and use method swizzleing to inject code. Because there is so much content, this tutorial will be explained very quickly. Before continuing, please make sure you have mastered the relevant iOS/OS X development. Developing plugins in Swift is still a complex topic, and Swift's debugging tools are still much weaker than Objective-C. For now, this means that the best choice for plugin development (for this tutorial!) is Objective-C. start To celebrate Prank Your Coworkers Day, your Xcode plugin will Rayroll your victims. Wait… What is Rayrolling? It’s a free and royalty-free Rayrolling Rick Shake – where you see something other than what you expect. When you complete this series, your plugin will change what Xcode displays: Replace some Xcode alert boxes with Ray's head (for example, Xcode prompt boxes for build success or failure) Replace the Xcode title with a line from Ray's hit song, Never Gonna Live You Up Replace all Xcode search document content with a video Rayroll'd video In the first part of the tutorial, we will focus on finding the class responsible for displaying the "Build Success" alert box and changing its image to Ray's head image. Install Plugin Manager Plugin Alcatraz Before you begin, you need to install Alcatraz, the Xcode plugin management tool. The typical way to install Alcatraz is through the command line
When this command is finished, restart Xcode. You may see an alert box prompting the Alcatraz bundle; click Load Bundle to continue so that Xcode can load the Alcatraz plugin so that the Alcatraz plugin can work. NOTE: If you accidentally click "skip bundle", you can always get it back by typing the following command from the command line!
The above Xcode 7.2.1 is the Xcode version number on your machine. If yours is not 7.2.1, just change it to the corresponding version number. You will see a new menu item under the Window menu of Xcode: Package Manager. To create an Xcode plugin, you need to set Build Settings to run another new instance of Xcode to load it. This is a boring and tedious process (if you want to know this boring process, you can refer to my article Xcode7 Plugin Making Primer). Fortunately, someone has already done this for you. Someone has developed an Xcode project template that allows you to easily create a plugin project. Open Alcatraz (Window->Package Manager). Type Xcode Plugin in the Alcatraz search box. Make sure you check both All and Templates in the search box. Once you find it, click Install to the left to install it!! If you can't find it, it doesn't matter. You can go to https://github.com/kattrali/Xcode-Plugin-Template and download it yourself to load it. For specific installation methods, see the project instructions. Once Alcatraz has downloaded the Xcode Plugin, you can create a plugin project (File->New -> Project…), select the new OS X ->Xcode Plugin ->Xcode Plugin template, and click Next. Name the project Rayrolling, set the organization identifier to com.raywenderlich (this step is very important), and select Objective-C as the language. Save the project in any directory you want. Hello World plugin template Compile and run the Rayroll project, and you will see a new Xcode instance appear. This Xcode instance has an additional menu item Do Action under the Edit menu bar: Selecting this menu item will cause a modal popup to appear: Starting from Xcode5, plugins can only run in a specific version of Xcode. This means that when a new Xcode update is installed, all third-party plugins will be invalid unless you add the UUID of that version of Xcode. If some templates do not work and you do not see a new menu item, one of the possible reasons is that there is no UUID for the corresponding version, and you need to add support for the corresponding version of Xcode. To add the UUID, first run the following command in the command line
This command will output the UUID of the current version of Xcode. Open the Info.plist file of the Rayroll project. Navigate to DVTPlugInCompatibilityUUID and add it Note: Throughout this tutorial, you will run and modify installed plugins. This will change Xcode's default behavior, and of course, it may also cause Xcode to crash! ! If you want to disable a plugin, you can manually delete it through the terminal.
Then restart Xcode Find a feature of Xcode that we want to modify The most direct and effective way to get what's happening behind the scenes is to register an NSNotification observer to listen to all Xcode events. By listening to these message notifications through Xcode, you will go deep into the internals of some internal classes. Open Rayrollling.m and add the following properties to the class:
This NSMutableSet is used to store all the NSNotification names printed out by the Xcode console Next, in initWithBundle:, after if (self = [super init]) {, add the following code:
Passing nil to the name parameter indicates that we need to listen to all NSNotifications of Xcode. Now, implement the handleNotification: method:
handleNotification: Checks if the obtained notification name is in notificationSet. If not, prints its notification name and the corresponding class of the notification in the console. Then add it to notificationSet. In this way, you will only see each type of notification once in the console. Next, find the declaration for adding a menu item and replace it with the following code:
This code simply changes the title of the NSMenuItem to let you know when you click it, and it resets the set object that holds the NSNotification. ***, replace the implementation code of doMenuAction with the following
This menu item will reset all notifications stored in the notificationSet property. The purpose of this is to make it easy for you to observe the notifications you are interested in in the console, without being overwhelmed by repeated messages in the console, so that you can focus more. Compile and run again, please make sure you can distinguish which is your project's Xcode (the parent Xcode) and which is the Xcode instance you debugged (the child Xcode). Why do you need to distinguish them? Because every time you debug again, the changes you make will take effect in the debugged Xcode, while the parent Xcode will only take effect after you restart it. In the child Xcode, just click some buttons, open some windows, browse around in the program, and you will see messages triggered in the console of the parent Xcode. Find and monitor compiled prompts Now that you have learned the basics of viewing notifications (NSNotification) raised by Xcode itself, you now need to find out specifically which class the tooltip that shows the compile status is associated with. Run the Xcode plugin, open any project in the child Xcode, and make sure bezel notifications are turned on in Xcode settings, both Succeeds and Fails bezel notifications. Of course, make sure you are operating the child Xcode instance! Reset notificationSet through the menu item Reset Logger you created in the edit menu of Xcode, and then run your sub-code (the above allows you to open any project in the sub-Xcode, now you run the opened project in the sub-Xcode) When the child Xcode project compiles (either successfully or unsuccessfully), pay attention to the console output in the parent Xcode. Take a quick look to see if there are any notifications that you can pay attention to. Can you find some notifications that deserve your further attention? The following may help you (In the original text, the following list is hidden, you can click to display it, the author encourages you to look for it yourself first, and if you can't find it, then open the following prompt. Due to the limitations of the markdown editor I use, I can't do this, so I just put it here) The following items deserve your further attention:
You should pick one of these and explore it further to see if you can get some important information from it. For example, what does NSWindowWillOrderOffScreenNotification do? Great, you chose to explore NSWindowWillOrderOffScreenNotification further. Go back to the parent Xcode file Rayrolled.m, locate the handleNotification: method, add a breakpoint to the first line of the method, and set the breakpoint as follows:
You now know that there is a class called DVTBezelAlertPanel, and more importantly, you know that there is an instance of this class in memory. Unfortunately, you can't find any header file about this class to tell you whether this class is the one that displays the Xcode alert box. In fact, you can still get this information. Although we don't have a header file for this class, if you have a debugger connected to the child Xcode, the information in memory can still tell you relevant information about this class, just like you read its header file. Note: Throughout this series of tutorials, LLDB output is usually accompanied by the standard console output. Any line beginning with (lldb) is considered an input line, where you can enter commands. Three dots... output in the console means that the console cannot print enough and ignores some of it. If the console displays too many print logs, you can simply press ? + K to clear the current output and re-accept the output Make sure your parent Xcode is in debugging mode, the program stops at the breakpoint, and enter the following lldb command into the parent Xcode's lldb console:
This command searches any frameworks, libraries, and plugins loaded into the Xcode process for information about a class named DVTBezelAlertPanel, and then outputs the information found. Observe the methods listed in the search results. Have you been able to find some methods that can be used to associate the DVTBezelAlertPanel class with the compilation success/failure alert box that appears in the child Xocde? Below I provide a list of some methods that may help you. (In the original text, the following list is hidden, you can click to display it, the author encourages everyone to look for it first, and if you can't find it, then open the prompt below. Due to the limitations of the Markdown editor I use, I can't do this, so I just put it here). Helpful methods The following methods of the DVTBezelAlertPanel class are worth further exploration:
Either of the two initialization methods above can basically help you verify whether the class DVTBezelAlertPanel is associated with the content of the prompt box that appears. Note: LLDB's image lookup command only lists methods that are implemented in memory. When you use this to look up certain classes, it does not include methods that are inherited from the parent class, but the subclasses do not override them, that is, it only lists the methods that they implement. Make sure you are still at the breakpoint in the parent Xcode and enter the following command in the parent class’s LLDB console to inspect contentView:
The console output is nil.(⊙o⊙)..., probably because the contentView has not been initialized at this time. It doesn't matter, let's try the next one: initWithIcon:message:parentWindow:duration and initWithIcon:message:controlView:duration:, because you already know that there is an instance of the DVTBezelAlertPanel class in the memory, which means that these two initialization methods have been called. You need to add debugging breakpoints to these two methods, because we don't have its implementation file, so here we use the LLDB console to add breakpoints. Then trigger the initialization of this class again. The parent Xcode is still stuck at the breakpoint, enter the following command
Yohunl Note: In Xcode 7.2.1, it is displayed as
This regular expression breakpoint will add a breakpoint to both initialization methods above, because both methods have the same starting character, and the regular expression will match both of them. Don't forget the \ symbol before the space in the regular expression above, and the single quotes ' to enclose the entire expression so that LLDB knows how to parse it. Switch to the child Xcode and recompile the child project (ctrl+B). The parent Xcode will hit the initWithIcon:message:parentWindow:duration breakpoint. If there is no *** breakpoint, check whether the breakpoint is set in the parent Xcode (if you set it in the child Xcode, it will not work), and whether a project is compiled in the child Xcode. Because the corresponding source code file cannot be found, Xcode will break in the assembly code of the method. Now that you have a breakpoint into a method without the source code, you need a way to print out the arguments passed to that method. It's time to talk about that. . . assembly... :]
Compilation Tour When you are dealing with private APIs, you often need to analyze registers instead of using debug symbols like you would when you have the source code. Understanding the behavior of registers in the x86-64 architecture will help you a lot. Although not a must-read, this is a very good article about x86 Mach-0 assembly. In the third part of this tutorial, you will go through some disassembly code of the method to understand what the method does. But for now, you just need a brief understanding. The following registers and how they work are worth your attention:
Note: The above description is not absolute. In some binaries, different registers are used to store different types of parameters, for example, doubles use the $xmm register bank. The above is just a quick reference! Below we use the following method to apply the above theory to practice
Use the following code to execute it:
After compilation, the call to the method aMethodWithMessage will be replaced by the Runtime layer with a call to objc_msgSend, which is basically similar to the following:
The call of aMethodWithMessage of aClass will change the contents of some registers: Before calling method aMethodWithMessage
When the calling method is finished
x86 Registers Now that you have a guide to registers, it's time to revisit the initialization method initWithIcon:message:parentWindow:duration: of DVTBezelAlertPanel. Hopefully, your parent Xcode breakpoint is still at this method. Of course, if not, it's okay, rerun the child Xcode and stop at the parent class breakpoint initWithIcon:message:parentWindow:duration: again. Remember, you are looking for clues between the class DVTBezelAlertPanel and the display of the Xcode compilation success/failure prompt box. When the program breakpoint is at initWithIcon:message:parentWindow:duration, enter the following in the LLDB console
This command is the abbreviation of register read, which is used to output the contents of important registers currently visible on your machine. Use what you have learned about x86 registers to check which register is used to store the message parameter and the fourth parameter of the objc_msgSend method. Is this the content we want to get in the alert box? (In the original text, the following list is hidden. You can click to show it. The author encourages you to find it yourself first. If you can't find it, open the prompt below. Due to the limitations of the markdown editor I use, I can't do this, so I just put it here) Yes, you should check the register $rcx, and you will see that its content is the content of the message parameter, which is the prompt message displayed in the Xcode compilation prompt box. Enter the following command to drill down further:
Note: Xcode outputs register contents in the default AT&T assembly format, in which the source and target operands are swapped, meaning that the first operand in AT&T syntax is the source operand, and the second is the destination operand, from left to right, which is the opposite of Intel's assembly format. (Translator's note: For more information about AT&T assembly, see http://blog.csdn.net/bigloomy/article/details/6581754) It looks like this is the register we are looking for! Try changing the content of $rcx to a new string and see if the content of the alert box changes:
The application will resume running. Pay attention to whether the content of the prompt box showing the success/failure of the compilation has changed to the string we modified. You will see that it has indeed become the new string we set, which also verifies our assumption that DVTBezelAlertPanel is used to display this prompt information. Code Injection Now that you have found the class you need, it is time to extend the behavior of DVTBezelAlertPanel through code injection to display the lovely Rayrolling (name) avatar in the compilation prompt box. We use the method swizzling technology. You may want to swizzle a lot of methods from many different classes, so the best advice is to create a category of NSObject and provide a convenience method in it to establish all the swizzle logic. In Xocde, select File\New\File…, then select OS X\Source\Objective-C File. Create a file called MethodSwizzler and make sure it is of the NSObject category. Open NSObject+MethodSwizzler.m and replace it with the following code:
The key codes are numbered and explained below:
Add the declaration of the method swizzleWithOriginalSelector:swizzledSelector:isClassMethod to the header file NSObject+MethodSwizzler.h as follows:
Next, we can do the actual swizzling. Create a new category called Rayrolling_DVTBezelAlertPanel, which is also a category of NSObject. Replace the created NSObject+Rayrolling_DVTBezelAlertPanel.m with the following code
The above code is relatively simple, let's analyze it:
Congratulations, you have successfully injected code into a private method of a private class! Compile the parent Xcode, then compile and run a project in the child Xcode, and check the console output of the parent Xcode to see if it is successfully wswizzled.
Next, you can replace the icons on the compilation success/failure prompt box with the Rayrolling avatar. Download the avatar resource Crispy from here, then add it to the project, making sure to select Copy Items if Needed. Now, navigate to the method Rayrolling_initWithIcon:message:parentWindow:duration and change its code to the following:
This method first checks if an image parameter is passed to the original method, and then replaces it with our custom image. Note: Here you use [NSBundle bundleWithIdentifier:@"com.raywenderlich.Rayrolling"]; to load the image, because Xcode's MainBundle does not contain our resources. Recompile the parent Xcode, then compile a project in the child Xcode, you will see Add a switch and persistence This plugin is designed for entertainment, so you definitely need a switch to make it work or not. We use NSUserDefaults to persist the variables that make it work or not. Navigate to Rayrolling.h and add the following code
Add in Rayrolling.m file
Now that you have the logic to persist your selections, it's time to hook it up to the GUI. Go back to Rayrolling.m and modify the code in -(void)doMenuAction to the following:
This is a bool value used to switch, enable or disable Rayrolling ***, change the initialization code of the menu item in didApplicationFinishLaunchingNotification: to the following:
This menu item will retain the logic you choose to enable or disable, even if Xcode is restarted, because your choice is already persistently stored. Navigate to the file NSObject+Rayrolling_DVTBezelAlertPanel.m and add a line to the header file
***, open the method Rayrolling_initWithIcon:message:parentWindow:duration:, and set
Replace with
Build and run the program to change the behavior of the plugin. Now, you have created a plugin that can be used to change the icon and content of the Xcode build success/failure prompt box, and it can also be turned on or off. This is a pretty good day's work, isn't it? ? ? What to do next? You can download the complete demo project from here. You’ve made a lot of progress, but there’s still a lot to do! In the second part of this tutorial, you’ll learn the basics of DTrace and dive into some advanced features of LLDB, such as finding running processes, such as the running Xcode process. If you want to go further, you still have some work to do before you move on to Tutorial 3. In Tutorial 3, you will see a lot of assembly code. Make sure you have started to understand the relevant x86_64 assembly knowledge before that. Here are 2 articles in Mike Ash's series of articles on assembly analysis, Article 1 and Article 2, which can provide you with relevant help. |
<<: Email inventor Tom Linson dies at 74
>>: Google Photos now supports Apple's Live Photos feature
Event planning refers to the planning of differen...
(1) After the Douyin account qualification review...
After the Shenzhou 16 manned spacecraft entered o...
The Spring Festival marketing war is about to beg...
Recently, many customers have sent private messag...
Live broadcast script is a key factor affecting t...
Wang Yuquan · Qianshao Technology Training Camp R...
In July 2023, the retail sales of passenger cars ...
Absolute stillness does not exist in the universe...
Recently, a netizen complained to Clippings that ...
How should the title be written? What psychologic...
The course comes from Yuting’s 2021 clothing live...
Nowadays, the application of 400 telephones in do...
As the saying goes, every festive occasion makes ...
If you think that cell phone explosions only happe...