日常碎碎念

今日來嘗試一下用純 C 寫個 iOS App, 純屬娛樂. 反正這個專欄就是記錄我各種奇怪想法的.

一個 iOS app 首先是用 main.m 內的 main 函數開始的. 現在就先創建 Single View App 項目, 然後把所有的 .m 文件都刪掉, 建一個 main.c 文件. 通常我們看到的 main.m 的內的代碼是這樣的

// main.m
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

那先照著這個寫就好了, 但是這個 @autoreleasepool 這個怎麼處理.

我們曉得這是個語法糖, 在 ARC 出來之後編譯器就不讓我們使用 NSAutoreleasePool, 原先是這樣的

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release];

那就仿照著這玩意寫成 C 版本的

int main(int argc, char **argv) {
id pool = objc_msgSend(
objc_msgSend((id) objc_getClass("NSAutoreleasePool"),
sel_registerName("alloc")),
sel_registerName("init"));
UIApplicationMain(argc, argv, nil, CFSTR("AppDelegate"));
objc_msgSend(pool, sel_registerName("drain"));
}

CFSTR 這個宏可以從 C 字元串創建一個 CFString 的引用(CFStringRef), 這玩意可以用來代替我們這裡的 NSStringFromClass([AppDelegate class]).

現在已經抄作業抄了一個 main.c, 不過還有個問題, UIApplicationMain 這個函數從哪裡跑出來的.

這個是一個用於創建我們應用實例的函數, 但是我們沒法直接使用它, 因為它是在 UIApplication.h 文件, 不過我們可以這樣搞(這裡順便把 runtime 那些頭文件補上吧)

#include <CoreFoundation/CoreFoundation.h>
#include <objc/runtime.h>
#include <objc/message.h>

extern int UIApplicationMain(int, ...);

int main(int argc, char **argv) {
id pool = objc_msgSend(
objc_msgSend((id) objc_getClass("NSAutoreleasePool"),
sel_registerName("alloc")),
sel_registerName("init"));
UIApplicationMain(argc, argv, nil, CFSTR("AppDelegate"));
objc_msgSend(pool, sel_registerName("drain"));
}

反正最後編譯鏈接的時候能通過的.

這裡再講一下 UIApplicationMain 這個函數, 它雖然有 int 類型的返回值, 但是它永遠不會返回.

然後這玩意的前兩個參數就不管了, 就是處理一下 main 函數傳進來的參數, 第三個參數是需要傳入 UIApplication 或者其子類的名稱, 這裡傳 nil 就默認用 UIApplication.

我們需要關注的是最後一個參數, 這個參數讓我們傳一個代理類的字元串, 就是給應用設置個代理, 也就是講接下來我們要實現一個代理類.

所以我們現在來創建個 AppDelegate.c 的文件. 繼續照之前的套路走, 先看 AppDelegate.m 代碼, AppDelegate 這個類有個 window 的屬性, 有下面這個函數

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}

我們先得實現一個 AppDelegate class 才行. 一般稍微了解過 NSObject 定義的都曉得, 每個類有個 isa 用來標記這個類是什麼, 具體怎樣就不解釋了, 反正很多 runtime 以及 header 文件的定義都能找到.

除了搞個 class, 我們還要實現那個 application:didFinishLaunchingWithOptions: 的函數

// AppDelegate.c
#include <objc/runtime.h>
#include <objc/message.h>
#include <CoreGraphics/CoreGraphics.h>

typedef struct AppDelegate {
Class isa;
id window;
} AppDelegate;

Class AppDelegateClass;

BOOL applicationDidFinishLaunchingWithOptions(
AppDelegate *self, SEL _cmd, void *application, void *options) {
self->window = objc_msgSend((id) objc_getClass("UIWindow"), sel_getUid("alloc"));
self->window = objc_msgSend(self->window, sel_getUid("initWithFrame:"),
(struct CGRect) {0, 0, 320, 568});
id viewController = objc_msgSend(
objc_msgSend((id) objc_getClass("UIViewController"), sel_getUid("alloc")),
sel_getUid("init"));
id view = objc_msgSend(
objc_msgSend((id) objc_getClass("View"), sel_getUid("alloc")),
sel_getUid("initWithFrame:"),
(struct CGRect) {0, 0, 320, 568});
objc_msgSend(objc_msgSend(viewController, sel_getUid("view")), sel_getUid("addSubview:"), view);
objc_msgSend(self->window, sel_getUid("setRootViewController:"), viewController);
objc_msgSend(self->window, sel_getUid("makeKeyAndVisible"));

return YES;
}
__attribute__((constructor))
static void initAppDelegate() {
AppDelegateClass = objc_allocateClassPair((Class) objc_getClass("UIResponder"), "AppDelegate", 0);
class_addIvar(AppDelegateClass, "window", sizeof(id), 0, "@");
// - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
class_addMethod(AppDelegateClass, sel_registerName("application:didFinishLaunchingWithOptions:"), (IMP) applicationDidFinishLaunchingWithOptions, "i@:@@");
objc_registerClassPair(AppDelegateClass);
}

這裡通過 __attribute__((constructor)) 這個編譯屬性讓這個函數在 main 函數之前走. 通過 runtime 搞了個 AppDelegateClass 出來. 由於我比較窮, 手機還是 iPhone 5s, 所以設了 (struct CGRect) {0, 0,320, 568}).

通過引入 CoreGraphics.h 才可以讓編譯通過 CGRect.

現在了解到創建一個 class 的套路之後, 這裡在 applicationDidFinishLaunchingWithOptions 使用到了 View class, 我們就創建一個 View.c 文件來自定義視圖什麼.

// View.c
#include <objc/runtime.h>
#include <CoreGraphics/CoreGraphics.h>

Class ViewClass;

extern CGContextRef UIGraphicsGetCurrentContext();

void viewDrawRect(id self, SEL _cmd, CGRect rect) {
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSetFillColor(context, (CGFloat[]) {1, 0, 0, 1});
CGContextAddRect(context, (struct CGRect) {0, 0, 320, 568});
CGContextFillPath(context);
}

__attribute__((constructor))
static void initView() {
ViewClass = objc_allocateClassPair((Class) objc_getClass("UIView"), "View", 0);
class_addMethod(ViewClass, sel_getUid("drawRect:"), (IMP) viewDrawRect, "v@:");
objc_registerClassPair(ViewClass);
}

這裡直接用 CoreGraphics 來繪製視圖.

然後編譯執行看看效果, 應該是一個空白的紅色視圖. 如果編譯出錯了, 可能是現在的 Xcode 禁止 objc_msgSend 函數的調用, 在 Build Settings 啟用它就好了.

忘了還有個事要做, 那就是把這幾個東西導入到項目中.


其實就是通過 runtime 來各種調用函數, 這個拿來玩玩就好了. 好吧, 先這樣吧.


推薦閱讀:
相关文章