阅读 900

纯 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 启用它就好了.

设置 Xcode objc_msgSend

忘了还有个事要做, 那就是把这几个东西导入到项目中

导入库

其实就是通过 runtime 来各种调用函数, 这个拿来玩玩就好了. 好吧, 先这样吧.

关注下面的标签,发现更多相似文章
评论