最近在做一些与ios7适配相关的工作,打算持续更新一些ios7适配的一些内容。

// TODO

注:文章转自:http://blog.cocoabit.com/blog/2013/12/08/meta-class/

英文原文地址:http://www.cocoawithlove.com/2010/01/getting-subclasses-of-objective-c-class.html

感谢原文作者以及翻译!

==============================================================

在这篇文章中,我关注的是 Objective-C 中的一个陌生的概念—— meta-class。在 Objective-C 中的每个类都有一个相关联的 meta-class,但是你很少会直接使用 meta-class,他们仍旧保持着神秘的面纱。我们从在运行时创建一个类开始。通过查看 “class pair”,我会解释 meta-class 是什么,同时也会谈谈在 Objective-C 中的对象或者类相关的一些一般主题。

在运行时创建一个类

下面的代码在运行时创建了一个 NSError 的子类同时为它添加了一个方法:

Class newClass = objc_allocateClassPair([NSError class],"RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);

添加的方法使用叫 ReportFunction 的函数作为实现,定义如下:

void ReportFunction(id self, SEL _cmd)
{
    NSLog(@"This object is %p.", self);
    NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);

    Class currentClass = [self class];
    for (int i = 1; i < 5; i++)
    {
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = object_getClass(currentClass);
    }

    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
} 

表面上来看,非常简单。在运行时创建一个类只需要这三步:

  1. 为 “class pair” 创建存储空间(使用 objc_allocateClassPair)。
  2. 为这个类添加所需的 methods 和 ivars(我已经使用 class_addMethod 添加过一个方法了)。
  3. 注册这个类,然后就可以使用了(使用 objc_registerClassPair)。

然后,中级问题是:“class pair” 是什么?函数 objc_allocateClassPair 只返回了一个值:这个 class。这一对中的另一个在哪?(译注:pair 有 “一对,一双” 的意思)

我敢肯定你已经猜到了另一半就是 meta-class(就是这篇文章的标题),但是要解释那是什么和你为什么需要它,我需要介绍一些在 Objective-C 中的关于对象和类的背景知识。

把一个数据结构变为对象需要什么?

每个对象都有一个类。这是面相对象概念的基础知识,但在 Objective-C 中不是这样,它(译注:class)同样是这个数据的一部分。每个可以被当成对象的数据结构都在恰当的位置有一个指向一个类的指针。

在 Objective-C,一个对象的类由它的 isa 指针决定。isa 指针指向这个对象的 Class。

事实上,在 Objective-C 中的对象的定义看起来像这样:

typedef struct objc_object {
    Class isa;
} *id;

这就是说:任何结构体只要以一个指向 Class 结构的指针开始的就可以被当成是 objc_object。

在 Objective-C 中的对象的一个重要的特性是,你可以向它们发送消息:

[@"stringValue"
    writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL];

你可以这么做是因为,当你向一个 Objective-C 的对象(像这里的 NSCFString)发送消息的时候,runtime 沿着对象的 isa 指针找到了这个对象的 Class(这里是 NSCFString 的类)结构体。 Class 结构体中包含了一个这个类的方法列表和一个指向父类的指针,用于查找继承的方法。

关键点是 Class 结构体中定义了你可以向一个对象发送的消息。

meta-class 是什么?

现在,你可能已经知道,在 Objective-C 中一个 Class 也是一个对象。这就意味着你也可以向一个 Class 发送消息。

NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];

这里,向 NSString 类发送了 defaultStringEncoding。

可以这么做是因为在 Objective-C 中每个 Class 它自己同样也是个对象。也就是说 Class 结构体必须以 isa 指针开始,然后就可以在二进制兼容(binary compatible)我上面介绍的 objc_object 结构了,接着下一个字段必须是一个指向它的父类的指针(要是类就是基类就是 nil)。

我上周已经介绍过,定义一个类有好几种方法,主要依赖于你正在运行的 runtime 的版本。但,是的,都是由一个 isa 字段开始然后是 superclass 字段。

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    /* 以下依赖于 runtime 的具体实现 …… */
};

然而,为了让我们在 Class 上调用一个方法,Class 的 isa 指针必须指向一个 Class 结构体,并且那个 Class 结构体必须包含我们可以在那个 Class 上调用的方法的列表。

这就引出了 meta-class 的定义:meta-class 是 Class 对象的类(the meta-class is the class for a Class object)。

简单来说:

[1] 当你向一个对象发送消息,就在那个对象的方法列表中查找那个消息。

[2] 当你想一个类发送消息,就再那个类的 meta-class 中查找那个消息。

meta-class 是必须的,因为它为一个 Class 存储类方法。每个类都必须有一个唯一的 meta-class,因为每个 Class 都有一个可能不一样的类方法。

meta-class 的类是什么?

meta-class,如之前的 Class,同样是个对象。这就意味着你也可以在它上面调用方法。自然的,这就意味着它也必须有一个类(译注:isa 指针)。

所有的 meta-class 使用它们基类的 meta-class (继承层次中最顶层的 Class 的 meta-class)作为它们自己的类。这就是说所有继承自 NSObject 的类(大部分的类),以 NSObject 的 meta-class 作为自己的 meta-class 的类。

遵循这个规则,所有的 meta-class 使用基类的 meta-class 作为他们的类,任何基类的 meta-class 将会是他们自己(它们的 isa 指向他们自己)。这就是说 NSObject 的 meta-class 的 isa 指针指向它们自己(是自己的一个实例)。

class 和 meta-class 的继承

和 Class 以 super_class 指针指向它的父类的方法一样,meta-class 以 super_class 指针指向 Class 的 super_class 的 meta-class。(译注:这句话有点绕,就是 super-class 一个指向 Class 的父类,一个指向 meta-class 的父类。Class 是一般对象的类型,meta-class 是 Class 的类型。)

进一步来讲,基类的 meta-class 设置 super_class 指针指向基类自己。

这个继承层次的结果就是,所有在这个继承层次中的的实例,类和 meta-class 都继承了基类的层次。

对于所有在 NSObject 层次中的实例,类和 meta-class,这就意味着所有 NSObject 的实例方法都是有效的。对于类和 meta-class,所有 NSObject 的类方法也同样是有效的。

所有这些在字面上相当让人困惑。Greg Parker 已经把实例,类,meta-class 还有他们的超类以非常棒的图解的方式聚合在一起,展示他们是如何在一起工作的。

用实验验证这点

为了验证这些,让我们看看在我文章开头提供的 ReportFunction 的输出。这个函数的目的是顺着 isa 指针打引出它找到的。

要运行 ReportFunction,我们需要为这个动态创建的类创建一个实例,然后在上面调用这个方法。

id instanceOfNewClass =
    [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];
[instanceOfNewClass release];

因为没有这个方法的声明,所以我使用 performSelector: 调用这个方法,这样编译器就不会输出警告了。

现在 ReportFunction 会沿着 isa 指针告诉我们这个对象使用了哪些类,meta-class 和 meta-class 的类。

获取一个对象的类:ReportFunction 使用 object_getClass 跟随 isa 指针,因为 isa 指针是一个类中一个受保护的成员变量(你不能直接访问其他对象的 isa 指针)。ReportFunction 没有以类方法的形式这样调用,因为在 Class 对象上调用类方法不会返回 meta-class,而是再次返回 Class 对象(所以 [NSString class] 会返回 NSString 的类而不是 NSString 的 meta-class)。

这个是程序运行后的结果(省去了 NSLog 的前缀)。

This object is 0x10010c810.
Class is RuntimeErrorSubclass, and super is NSError.
Following the isa pointer 1 times gives 0x10010c600
Following the isa pointer 2 times gives 0x10010c630
Following the isa pointer 3 times gives 0x7fff71038480
Following the isa pointer 4 times gives 0x7fff71038480
NSObject's class is 0x7fff710384a8
NSObject's meta class is 0x7fff71038480

看看通过递归的查看 isa 的地址:

  1. the object is address 0x10010c810.
  2. the class is address 0x10010c600.
  3. the meta-class is address 0x10010c630.
  4. the meta-class’s class (i.e. the NSObject meta-class) is address 0x7fff71038480.
  5. the NSObject meta-class’ class is itself.

地址的值不是很重要,只是演示了上面讨论的从类到 meta-class 到 NSObject 的 meta-class 的过程。

结论

meta-class 是 Class 对象的类。每个 Class 都有个不同的自己的 meta-class(因此每个 Class 都可以有一个自己不同的方法列表)。也就是说每个类的 Class 不完全相同。

meta-class 总是会保证 Class 对象会有从基类继承的所有的的实例和类方法,加上之后继承的类方法。如从 NSObject 继承的类,就意味着在所有的 Class(和 meta-class)对象中定义了所有从 NSObject 继承的实例和协议方法。

所有的 meta-class 使用基类的 meta-class(NSObject 的 meta-class 用于继承自 NSObject 的类)作为他们自己的类,包括在运行时自己定义的基础的 meta-class。

上一篇讲了Object-c的函数调用机制,对理解Method Swizzling有很大帮助。这一次来看一看Object-c中的黑魔法——Method Swizzling。

Method Swizzling介绍

什么是Method Swizzling?简单来讲,就是修改SEL与IMP之间的映射关系,达到修改最终执行方法的目的。Method Swizzling,听起来就有黑魔法的意味。

如何实现Method Swizzling

1.创建一个category

#import <Foundation/Foundation.h>  
@interface UIView (MyAdditions)   
@end  

2.创建一个swizzling函数

@implementation UIView (MyAdditions)

- (void)my_setFrame:(CGRect)frame {
    // custom function
    [self my_setFrame:frame];
}
@end

等等,这不是循环调用吗?别忘了,我们后面会施展黑魔法,改变IMP,调用的不再是自己,不会有循环调用的问题。

3.交换IMP

注意别忘记引入:#import <objc/runtime.h>

Method oriMethod =  class_getInstanceMethod([UIView class], @selector(setFrame:));  
Method myMethod = class_getInstanceMethod([UIView class], @selector(my_setFrame:));  
method_exchangeImplementations(oriMethod, myMethod); 

这样调用 [UIView setFrame:] 时,实际调用的是自定义的方法 [UIView my_setFrame:]。

Method Swizzling的利与弊

这里有非常好的答案:

http://stackoverflow.com/questions/5339276/what-are-the-dangers-of-method-swizzling-in-objective-c

其中给出了一个解决命名冲突的定义方式:

@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
   // do custom work
   SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
   [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end

typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

总结

对于我个人而言,并不是非常喜欢这种黑魔法。感觉还是慎用为妙,如果使用,尽量仅在load中swizzling。另外,网上有人说使用了Method Swizzling无法通过appstore的审核,还没有验证过。 虽然不建议发布使用,但是其他地方还是有很多用武之地的,比如调试、代码分析等,可以自由想像。

这里有一个MethodSwizzlingDemo,通过Method Swizzling,描绘每一个View的border。效果图:

起因

有一次,被问到,下面这段代码执行,会得到什么结果:

@interface ClassA : NSObject  
-(void) foo;  
@end  
@implementation ClassA  
-(void) foo  
{  
    NSLog(@"fork Class A's function");  
}  
@end  

@interface ClassB : NSObject  
-(void) foo;  
@end  
@implementation ClassB  
-(void) foo  
{  
    NSLog(@"fork Class B's function");  
}  
@end  

ClassA* a = [[ClassA alloc] init];  
[a foo];  
ClassB* b = (ClassB*)a;  
[b foo];

说来惭愧,这么久竟然不清楚object-c的函数调用机制,于是网上查阅了一些资料,整理出一篇文章。

Object-C的函数调用的机制

OC的方法调用与C++不同: C++中的方法调用可能是动态的,也可能是静态的;而ObjC中的函数调用都为动态的,通过消息传递的方式调用。

首先通过command+shift+o,查找NSObject类,可以看到类的定义:

@interface NSObject <NSObject> {  
Class isa  OBJC_ISA_AVAILABILITY;  
}  

typedef struct objc_selector *SEL;  

typedef id (*IMP)(id, SEL, ...);  

在object-c中,每个对象都有一个isa成员,指向自己的类结构体,这也是object-c自省的特性实现。 为了理解消息调用机制,需要理解以上三个定义:Class SEL IMP

Class SEL IMP

先来看一看Class的定义:

struct objc_class {  
Class isa  OBJC_ISA_AVAILABILITY;  

#if !__OBJC2__  
Class super_class                                        OBJC2_UNAVAILABLE;  
const charchar *name                                         OBJC2_UNAVAILABLE;  
long version                                             OBJC2_UNAVAILABLE;  
long info                                                OBJC2_UNAVAILABLE;  
long instance_size                                       OBJC2_UNAVAILABLE;  
struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;  
struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;  
struct objc_cache *cache                                 OBJC2_UNAVAILABLE;  
struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;  
#endif  

} OBJC2_UNAVAILABLE; 

其中我们比较关心的是参数列表ivars,方法列表methodLists,协议列表protocols。当一个类有调用方法时:

[receiver message]

会调用消息函数,objc_msgSend:

objc_msgSend(receiver, selector)

如果有参数要传递的话:

objc_msgSend(receiver, selector, arg1, arg2, ...)

Selector

当一个消息传递给一个对象的时候,objc_msgSend根据对象的isa指针,到类结构体methodLists链表找指定selector,如果没有,向父类寻找,知道找到NSObject类位置。这样就实现了动态方法调用。

那么IMP又是什么呢? 简单来说,就是一个函数指针,指向SEL的方法的地址。NSObject 类中的methodForSelector:方法就是这样一个获取指向方法实现IMP 的指针。 下面的例子展示了怎么使用指针来调用setFilled:的方法实现:

void (*setter)(id, SEL, BOOL);  
int i;  
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];  
for ( i = 0 ; i < 1000 ; i++ )  
    setter(targetList[i], @selector(setFilled:), YES); 

查找IMP 时:

  1. 首先去该类的方法 cache 中查找,如果找到了就返回它;

  2. 如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将它加入cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次调用再次查找的开销。

  3. 如果在该类的方法列表中没找到对应的IMP,在通过该类结构中的super_class指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的IMP,返回它,并加入cache中。

  4. 如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则进入下文中要讲的消息转发流程。

这里可以做一个延伸:如何提高调用Object-C方法的速度?

利用“IMP Caching”的方法,在框架初始化时获取类名、方法名和方法运行时的函数指针IMP。在执行这个函数的时候,使用Caching的指针执行,一般速度要提高2倍左右。

消息转发

通常,给一个对象发送它不能处理的消息会得到出错提示,然而,Objective-C运行时系统在抛出错误之前,会给消息接收对象发送一条特别的消息forwardInvocation来通知该对象,该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。

我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

NSObject 的forwardInvocation:方法默认的调用doesNotRecognizeSelector:方法

0   CoreFoundation                      0x01a1c5e4 __exceptionPreprocess + 180  
1   libobjc.A.dylib                     0x015728b6 objc_exception_throw + 44  
2   CoreFoundation                      0x01ab9903 -[NSObject(NSObject) doesNotRecognizeSelector:] + 275  

重写forwardInvocation:

-(void) helloWorld
{  
     NSLog(@"Hello world!");
}  
- (void) forwardInvocation:(NSInvocation *)anInvocation  
{  
    if([self respondsToSelector:@selector(helloWorld)])  
     {  
          [self helloWorld];  
     }  
    else  
    {  
        [super forwardInvocation:anInvocation];  
    }  
}  
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector  
{  
    if ([self respondsToSelector:aSelector]) {  
        return [super methodSignatureForSelector:aSelector];  
    }  
    else {  
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];  
    }  
}  

执行结果:

2013-11-23 15:23:50.464 VCTransitionDemo[1272:70b] Hello world! 

注意:我们重写forwardInvocation方法的时候,还必须同时重写methodSignatureForSelector方法,该方法返回表示一个方法的字符串,具体如何使用请看Type Encodings。

小结

到这里,应该能够回答最初的那个问题了。

OC扩展了标准的ANSI C编程语言,将Smalltalk式的消息传递机制加入到ANSI C中。它的方法调用与C/C++还是有很大差别的。

接下来,会再深入OC的方法调用,写一些关于Method Swizzling的内容。

参考资料:

Objective-C Runtime Reference

Type Encodings

octopress&github

感谢破船之家,分享了一篇关于使用octopress在Github上搭建博客文章,看了之后也尝试搭建了一个属于自己的博客。由于对于Ruby和git不是很了解,整个过程花费了不少时间。这篇文章主要记录MAC下搭建的过程。

1. 安装Git

首先在Github上创建一个账号,下载最新版本的Git,并安装。

参见:Set up Git

2. 使用RVM安装Ruby1.9.3

使用ruby —version查看当前Ruby版本。

$ ruby --version
ruby 2.0.0p247 (2013-06-27 revision 41674)

这里有一点要注意,必须是Ruby1.9.3版本才行。我的mac自带的Ruby2.0.0版本,安装octopress总是提示出错,原因我还不清楚。

使用RVM(Ruby Version Manager)来管理Ruby安装。首先安装RVM:

curl -L https://get.rvm.io | bash -s stable --ruby

然后使用RVM安装Ruby1.9.3版本:

rvm install 1.9.3
rvm use 1.9.3
rvm rubygems latest

然后再次查看Ruby版本:

ruby --version
ruby 1.9.3p484 (2013-11-22 revision 43786)

参见:Installing Ruby With RVM

3. 安装octopress

git clone git://github.com/imathis/octopress.git octopress
cd octopress

然后安装依赖,如果没有使用rbenv,第二句可以忽略:

gem install bundler
rbenv rehash    # If you use rbenv, rehash to be able to run the bundle command
bundle install

然后安装默认的主题:

rake install

参见:Octopress Setup

4. 配置octopress

octopress已经尽量简化需要配置的内容,下面列出了可能需要配置的文件:

_config.yml       # Main config (Jekyll's settings)
Rakefile          # Configs for deployment
config.rb         # Compass config
config.ru         # Rack config

主要在_config.yml中有需要配置的内容。Rakefile中的配置几乎都与部署有关,如果不是用rsync的话,不需要修改它。我实际上就没有对它做修改。

_config.yml中url一定要修改,title、subtitle、author和支持的第三方服务可以选择修改。

url:                # For rewriting urls for RSS, etc
title:              # Used in the header and title tags
subtitle:           # A description used in the header
author:             # Your name, for RSS, Copyright, Metadata
simple_search:      # Search engine for simple site search
description:        # A default meta description for your site
date_format:        # Format dates using Ruby's date strftime syntax
subscribe_rss:      # Url for your blog's feed, defauts to /atom.xml
subscribe_email:    # Url to subscribe by email (service required)
category_feeds:     # Enable per category RSS feeds (defaults to false in 2.1)
email:              # Email address for the RSS feed if you want it.

建议把twitter相关的信息删除,否则回加载很慢;/source/_includes/custom/head.html 把google的自定义字体去掉。

注意,分号后面必须有一个空格分隔,否则会出错。

参见:Configuring Octopress

5. 将博客部署到Github上

在Github上创建一个仓库,命名为:username.github.io或organization.github.io (username为你的用户名)

然后终端输入:

$ rake setup_github_pages

会让你输入仓库路径,安装提示输入即可。

然后:

rake generate
rake deploy

这样就会生成你的博客,拷贝在_deploy/目录下。

这里要注意的是,如果rake deploy执行提示出错

! [rejected]        master -> master (non-fast-forward)

由于master分支在_deploy/目录内,所以:

cd octopress/_deploy
git pull origin master
cd ..
rake deploy

将博客的source提交到gitub上:

git add .
git commit -m 'your message'
git push origin source

参见:Deploying to Github Pages

6. 开始写博客

rake new_post["title"]

创建一个名称为title的博客,可以在source/_posts下找到按照Jekyll的命名规范新生成的文件,打开可以看到:

---
layout: post
title: "title"
date: 2011-07-03 5:59
comments: true
external-url:
categories:
---

生成并预览:

rake generate   # Generates posts and pages into the public directory
rake watch      # Watches source/ and sass/ for changes and regenerates
rake preview    # Watches, and mounts a webserver at http://localhost:4000

可以本地打开http://localhost:4000 ,预览生成的博客。

最终,部署到Github上:

$ git add .
$ git commit -am "Some comment here." 
$ git push origin source
$ rake deploy

参见:Blogging Basics

7. 小结

至此,使用octopress搭建的博客就已经完成了,之后可以选择新的主题,做一些个性化的配置。另外推荐一本学习git的好书:Pro Git

程序猿一枚

爱旅游 爱科幻 弹弹吉他

爱看杂七杂八的书,寻求人生答案

每天都在体验无知所带来的痛苦和快乐

也许有一天能够习得屠龙之技,窥破宇宙之道


我的二维码