objective-c 与java比较

Quick Start

1、Cocoa是什么?Cocoa是使用OC语言编写的工具包,里面有大量的类库、结构体,说白了其实就相当于java中的标准API、C++中的标准库。OC中没有命名空间的概念,所以使用加前缀来防止命名冲突,因此你会看到大量的以NS 为前缀的类名、结构体、枚举等。

2、Cocoa框架由Foundation Kit、App Kit两部分组成,前者是基础工具库,后者主要是UI库、高级对象等。

3、static 标识的类变量定义在接口的外面,类变量只能本类访问,除非提供类方法给外部访问这个类变量。

4、@语法是OC特有的一种语法,C是没有的。

5、OC中只有类的成员变量才有访问权限控制,@public、@protected、@private,默认是@protected,类变量、类方法、成员方法是没有访问修饰符的,所有的方法都是public的,所有的类变量都是私有的。

6、OC中的类方法只能类调用,如果使用对象调用就会报错,而java这只是一个警告而已。

7、OC中定义类的@interface和java中的interface不是一回事,OC的@protocol和java中的interface才是一码事。@interface跟java和C++ class关键字差不多,在OC和C++中,都习惯将类定义写在头文件里,而java却根本没有头文件的概念,至于为什么有这样的差别,我想是为了OC和C++编译器方便,大多数时候编译器只需要知道头文件类的定义就知道这个类有哪些属性和方法了。

8、get为前缀的方法在OC中有特殊的意义,所以尽量使用成员变量名称来作为getter方法的函数名。

9、OC是动态语言,所以即便是@interface中没有定义的方法在.m文件中实现了也是OK的,一般我们都是给别人提供头文件,这种情况就相当于把方法变相的私有化了,对于我这种喜欢在写代码过程中随时加方法但又不想动头文件的人来说还是比较爽的。

10、在OC中所有对象的存取都是用指针,C++中还有引用,OC中没有,我宁愿你强迫我使用一种方法,也不愿你提供给我两种方法我却不知道使用哪一种方法的好。

11、OC的方法定义和调用算是一大“特色”:
方法的定义:-(返回类型) 方法名:(参数1类型)参数1变量 参数2标签:(参数2类型)参数2变量…
[类或者实例的指针 方法名: 参数1 标签2: 参数2… …],
为了好看,第一个参数一般不加标签名,当然,标签名都可以隐藏的,但不建议这样做,因为当你接手了一个离职的人程序,其中的JAVA 程序调用了一个有五个甚至更多的参数的方法,但是你手里没有这个方法的API,那么你很难猜得出来这五个参数到底都干什么用的,但是Objective-C调用的时候,每个参数前面都必须有方法的标签名,这样你便能很容易的从标签名看出这个参数是什么意思。

12、调用类方法:
[Fraction t];
[[Fraction class] t];
Class clazz=[Fraction class];[clazz t];
class 来自于NSObject,相当于JAVA 中的getClass()方法,也就是获取这个类的Class 对象,
clazz 前面没有*,这是因为Class 已经是一个指针。另外这种嵌套调用的方式,你也要习
惯,这就和JAVA 中的A.b().c()没有什么区别。
获取Class 有如下几种方法:
[类或者对象 class]
[类或者对象 superclasss]
NSClassFromString(类名的字符串形式)
你也可以通过如下的函数把Class 转换为字符串形式:
NSStringFromClass(Class)

13、OC中自定义类最好显示继承NSObject,可以获得alloc、init、release等方法。

14、OC中使用nil表示null,但跟java中的null还是有区别的,java中调用null的方法,会报臭名昭著的空指针异常,而OC不会,所以在OC你不必再为空指针而烦恼。

15、Cocoa中提供的很多函数和java中的api有很大的不一样,java是纯面向对象的,所有的方法都必须在类中定义,但OC是兼容C语言的,所以C中的面向过程思想在OC中也是行得通的,因此Cocoa中很多东西都不是对象,而是C语言的函数、结构体,比如NSLog()。

16、对象的初始化
前面我们看到实例化对象最多的方法是:Fraction *frac=[[Fraction alloc] init];
这跟java不一样,java对象创建只需要new一下,同时调用构造方法(实际上虚拟机层面分为两个步骤:new对象,执行方法(包含构造方法)),但是OC中分为两步:分配内存(同时给变量赋初值)、初始化。
(1)、alloc 是从NSObject 继承而来的类方法,用于给对象分配存储空间,所有的成员变量在此时对确定了自己的内存位置,并被赋初值,整数类型为0,浮点数为0.0,BOOL 为NO,对象类型为nil,alloc 方法返回对象的指针。
(2)、init这个方法是从NSObject继承而来的,你可以覆盖它,当然init不是构造方法,只是一个普通方法而已,你甚至可以换成其他方法,只不过我们习惯在方法名前缀加上init。
还记得java中的构造方法没有返回值吧,所以为了达到java这种效果:Test t = new Test(2);OC中需要你手动在普通方法(相当于构造方法)中return self。

17、NSObject中的description方法相当于java中Object的toString方法,,用于获取对象的字符串表示。唯一不同的是OC中需要使用格式化字符串%@,才会触发description被调用:
Fraction *frac=[[Fraction alloc] init];
NSLog(@”%@”,frac);

18、Objective-C 的异常都继承自NSException,当然NSException 本身也继承自NSObject。使用下面的语句块进行异常捕获:
@try {
<#statements#>
}
@catch (NSException *exception) {
<#handler#>
}
@finally {
<#statements#>
}
总体来说,OC中的异常体系和java中的异常体系差不多,唯一的差别就是java中有业务异常,意思就是说你必须捕获他,如果不捕获就会出现编译错误,OC中的异常基本上都属于java中的运行时异常,如果你没有捕获,发生了异常程序就会崩溃,在一些循环批任务中尤其要注意。

18、OC中还有一个特色那就是他的弱类型id,他可以表示任何对象,实际上应该是利用了指针地址的通用性,由于OC中操作对象都必须是指针,而指针本身又是一串地址,所以通过指针可以指向一切不同的对象。

19、继承(感觉和java差不多啊)

20、反射(这个和异常一样,感觉跟java也很像)

21、选择器(其实就是C中的函数指针,以及java中的Method类)

22、类别(Category),扩充类的功能,但不可以声明新的成员变量。
类别的应用比较广泛,譬如:第三方的Objective-C 库RegexKitLite 就是对NSString、NSMutableString 做的类别,为Objective-C 的字符类型增加了正则表达式的功能。

23、协议@protocol,这个就是前面说的,相当于java中的interface,思想都是一种约定、规范,只是具体语法不一样罢了。

24、终于到了内存管理了,擦,没怎么看懂,要好好找个专题弄懂。

25、字符串
-(BOOL) isEqualToString: (NSString) s
比较两个字符串是否相等,与JAVA 一致的地方是==比较指针,比较对象是否相同要用到equal 方法。
-(NSMutableString
) appendString: (NSString*) s
这与JAVA 的StringBuffer 的append 没什么区别。

26、数组
Cocoa 使用NSArray 表示数组,但是它不能存储基本数据类型、enum、struct、nil,只能存储Objective-C 的对象。
NSArray array=[NSArray arrayWithObjects: @”One”, @”Two”, @”Three”, nil];
从这个类方法arrayWithObjects 的定义可以看出,它使用nil 表示数组元素结束,这也是nil 不能存储在NSArray 中的原因。
NSMutableArray 为长度可变的数组,相当于JAVA 中的List:
NSMutableArray
mArray=[NSMutableArray arrayWithCapacity: 10];
[mArray addObject: @”Apple”];//添加数组元素
NSEnumerator e = [mArray objectEnumerator];//获取数组的迭代器,相当于JAVA 中的Iterator,reserveObjectEnumerator 用于获取反转之后的数组迭代器。与JAVA 一致的地方是你在使用迭代器时,不能对数组进行添加、删除操作。
for(NSString
ms in mArray){
NSLog(@”%@”,ms);
}
//for-each 快速枚举为Objective-C 2.0 的新特性,Windows 上的GNUStep 并不支持。

27、字典(哈希表)
NSDictionary 用于存储key-value 的数据结构,与JAVA 中的Map 类似。
NSDictionary dic=[NSDictionary dictionaryWithObjectsAndKeys: @”Apple”, @”A”, @”Google”,@”G”, nil];//dictionaryWithObjectAndKeys 后的可变参数,每两个为一个value-key,以nil 表示结束。
NSLog(@”%@”,[dic objectForKey: @”A”]);//按照指定的key 查询value
同样的有NSMutableDictionary 表示长度可变的字典。
NSMutableDictionary
mDic=[NSMutableDictionary dictionaryWithCapacity: 10];
[mDic setObject: @”Apple” forKey: @”A”];//添加value-key 对
for(id key in mDic){
NSLog(@”%@ : %@”,key,[mDic objectForKey: key]);
}
//快速迭代的for-each 循环

28、哈希Set
NSSet 表示以hash 方式计算存储位置的集合,与JAVA 中的HashSet 是一致的。在NSSet 中的每个对象都有一个唯一的hash 值,重复的对象将只能保留一个。因此,这引出了Objective-C中的对象比较问题,这需要你实现从NSObject 继承而来的如下两个方法:

  • (BOOL) isEqual: (id) anObject;
  • (NSUInteger) hash;
    这与JAVA 的对象比较没有什么区别,两个相等的对象必须有相同的hashCode,所以这两个方法必须同时实现。
    同样的,NSMutableSet 表示长度可变的哈希Set。

29、封装类(相当于java的包装器)
前面的几个容器类的对象都不能存放基本数据结构、enum、struct、nil,怎么办呢?在JAVA中我们知道所有的基本数据类型都有对应的封装类,例如:int—Integer、boolean—Boolean,使用封装类可以把基本数据类型包装为对象,而封装类本身又有方法可以返回原来的基本数据类型。Cocoa 使用NSValue 作为封装类。
NSRect rect=NSMakeRect(1,2,30,50);//这个是在前面提到过的一个表示矩形的结构体
NSValue *v=[NSValue valueWithBytes: &rect objCType: @encode(NSRect)];
//valueWithBytes 要求传入包装数据的地址,也就是变量中存储的数据的首地址,我们依然使用C 语言的&作为地址运算符,计算指针存储的首地址。
//objCType 要求传入用于描述数据的类型、大小的字符串,使用@encode 指令包装数据所属的类型。

NSMutableArray mArray=[NSMutableArray arrayWithCapacity: 3];
[mArray addObject: v];
NSRect rect2;
[[mArray objectAtIndex: 0] getValue: &rect2];
//NSValue 的-(void) getValue: (void
) value 方法可以将NSValue 中的数据内容取出,要求传入的参数是一个指针,也就是说getValue 方法将你传入的指针指向存储的数据。
//一般Cocoa 中的getXXX 方法都是这个作用,这也是为什么前面的getter 方法不要以get作为方法前缀的原因。
对于基本数据类型,你可以使用简便的NSNumber 来封装,它是NSValue 的子类。
如果你想存储空值到集合类,可以使用NSNull,它使用NSNull *n =[NSNull null];的方式创建。

30、日期类型
Cocoa 中使用NSDate 类型表示日期。

31、数据缓冲区(其实就是java中的字节数组)
Cocoa 中使用NSData 类型来实现缓冲区,用于存储二进制的数据类型,譬如:从网络下载回来的文件等。
NSData 是长度不可变的数据缓冲区,还有一个NSMutableData 用来存储长度可变的数据缓冲区。

32、写入和读取属性(简直就是java序列化和反序列化的翻版)
在iPhone 的.ipa 文件中,你经常可以看到.plist 文件,它保存了程序的相关属性,叫做属性列表。其实它就是NSArray、NSDictionary、NSString、NSData 持久化之后的文件。
这几个类型都有一个成员方法writeToFile: (NSString) file atomically: BOOL 用于将自己写入到一个文件。atomically 为YES 表示文件先存储到临时文件区,如果文件保存成功,再替换掉原始文件,这样的好处是可以防止在保存文件过程中出错,但是仅适用于小文件,因为你相当于是在临时文件存储区也放了一份,再加上原始文件,就是两份文件,如果文件比较大,将会占用较多的用户的磁盘空间。
如果你要持久化的类型不是上述的数组、字典、缓冲区,那该怎么办呢?Objective-C 中也有和JAVA 一样的序列化、反序列化支持,使用NSCoding 协议。NSCoding 协议定义了如下两个方法:
-(void) encodeWithCoder: (NSCoder
) coder;
-(id) initWithCoder: (NSCoder*) decoder;
第一个方法相当于编码对象,第二个方法是解码回对象,也就相当于给类型定义了一个新的init 方法而已。

33、对象的复制
对象的复制就相当于JAVA 中的clone()方法,也就是对象的深度复制,所谓深度复制就是重新分配一个存储空间,并将原对象的内容都复制过来,从这些描述可以看出,复制也会分配空间,那就是你要对复制出来的对象release,就是前面所说的alloc、new、copy 操作创建的对象,要手工release。
Objective-C 中的一个对象是否可以被复制,要看它的类型是否遵循NSCopying 协议,这个协议中有个复制方法-(id) copyWithZone: (*NSZone) zone 需要我们去实现。

34、多线程
Objective-C 的多线程编程与JAVA 语言极其类似分为原始的线程操作、线程池两种,后者其实就是使用队列等机制对前者的封装。JAVA 也一样,原始的线程操作使用Thread 类,线程池的操作使用java.util.concurrent.中的类库。
Objective-C 中NSThread 类型表示线程,NSCondition 用于执行同步操作,在功能和用法上相当于JAVA 中的java.util.concurrent.
包中的Lock 对象。
另外,Objective-C 也支持@synchronized 指令做代码同步,写法也和JAVA 中的很相似。
@synchronized(要加锁的对象){
//同步执行的代码
}

另外比较有趣的是,如果你想更新UI 上的某一个部件,就需要在发起的新线程里调用UI 所在的主线程上的一个方法,新线程不能直接访问主线程的方法,需要在run 方法中使用如下的方法:

  • (void) performSelectorOnMainThread: (SEL) aSelector withObject: (id) arg waitUntilDone: (BOOL) wait
    如果你对java的swing熟悉的话,你应该知道在UI界,大多数更新界面的都必须在一个单线程中执行,因为如果更新界面是多线程的话会很难处理的,java swing处理的方法是更新界面的代码放在专门更新UI的线程下执行,其实这里IOS也是这么一个思想。

Objective-C 使用NSOperation、NSOperationQueue 两个类型实现线程池的操作,NSOpertion就好比JAVA 中的Runnable,其中的main(名字有点儿奇怪)方法也就是你要执行的操作,NSOperationQueue 是一个线程池队列,你只需要把NSOperation 添加到NSOperationQueue,队列就会retain 你的NSOperation,然后执行其中的操作,直到执行完毕。队列中会有多个操作,队列会按照你添加的顺序逐个执行,也就说队列中的任务是串行的。
跟java一样,如果线程池可以满足你的业务需求,尽量使用线程池,而不是原始的NSThread,因为使用线程池屏蔽了许多线程自身需要处理的问题,代码也更加简洁。

35、KVC 与KVO(这个话题真高级)
KVC 是NSKeyValueCoding 的缩写,它是Foundation Kit 中的一个NSObject 的Category,作用你可以类比JAVA 中的反射机制,就是动态访问一个对象中的属性。
KVC 在解析key 的字符串的时候,是会比你正常调用setter、getter 要慢的,而且编译器无法在编译器对你的方法调用做出检查(因为你的属性名都是字符串,只有运行时才会知道你有没有写错),出错的几率也会提高,所以请不要随意使用KVC,而省去setter、getter 方法。KVC 一般用于动态绑定,也就是运行时才能确定谁调用哪个方法,编译期并不确定。
KVO就是NSKeyValueObserving的缩写,它也是Foundation Kit中的一个NSObject的Category,KVO 基于KVC 实现,基于观察者设计模式(Observer Pattern)实现的一种通知机制,你可以类比JAVA 中的JMS,通过订阅的方式,实现了两个对象之间的解耦,但又可以让他们相互调用。(具体请查资料)

36、谓词NSPredicate
Cocoa 提供了NSPredicate 用于指定过滤条件,谓词是指在计算机中表示计算真假值的函数,它使用起来有点儿像SQL 的查询条件,主要用于从集合中分拣出符合条件的对象,也可以用于字符串的正则匹配。

iOS crash log 分析方法

Quick Start

当发布到iPhone上的应用程序Crash之后,iPhone会自动生成一个Crash Log(.crash),这个文件包含了一些有用的调试信息,但对于堆栈,它只记录的函数地址,而无法显示函数名。函数名保存在一个叫dSYM的二进制文件中,即一个调试符号表文件。要解析出Crash log中的函数名称,必须要这个dSYM文件和可执行程序(即那个.app包)。而且这三者必须严格对应,也就是说他们包含的UUID号必须一样。

可以用MAC自带的dwarfdump程序来检查UUID:

使用dwarfdump检查app,看哪个app是上面那个UUID。命令行格式:
dwarfdump –uuid YourApp.app/YourApp
用dwarfdump检查dSYM文件是否是上面的UUID。命令行格式:
dwarfdump –uuid YourApp.app.dSYM

这两个UUID必须一样,而且必须跟Crash Log里面的UUID一致。打开Crash Log文件,在”Binary Images:”段中,YourApp后面的尖括号内的字符串就是UUID了。
crash log uuid : armv7 <1dd5eb047491310f88cb7b1d7f61275c> /var/mobile/Applications/581404D9-FF06-455F-8251-846D41D18B40/

有了这三样东西,就可以用Xcode的Organizer来查看包含了符号信息的Crash log了,这个过程也称为symbolication。方法是:

先将app和dYSM两个文件拷贝到用户目录下的任意子目录中(我试过任意两层目录),然后在Organizer中选中“Device Logs”,接下来将Crash log文件拖动到这个窗口中,Organizer将开始寻找对应的文件,然后进行转换,转换后保存在它自己的一个目录下(在窗口中的log上右键点击选“Reveal Log inFinder”可以看到转好的文件)。

但随之而来的问题是,我们收到的程序崩溃调试信息多半是汇编语言一样的堆栈代码,同时这些信息并不是在我们debug的时候产生,所以看到这一串crash log的天书,常常无可奈何。Xcode很好的解决了这一问题,它所提供的Orgainzer分析器加上symbolicatecrash,可以分析二进制文件以及源代码和crashlog之间的连接,直接找出源程序中出错的代码行。方法网上到处是,本文不讨论。
但是如果使用symbolicatecrash无法定位到出错的代码行的话,怎么整呢?有一个办法,如下:
首先查看crash log中的崩溃线程,假如是这样的:
Thread 0 Crashed:
0 libobjc.A.dylib 0x00003ec0 objc_msgSend + 24
1 MyApp 0x000036d2 0×1000 + 9938
我们得到了用户发生崩溃情况的内存地址:0x000036d2
然后回到我们应用程序的build目录,目录下一定要包含MyApp.app 和MyApp.app.dSYM两个文件。
在控制台使用dwarfdump命令,解析出内存地址,如:
dwarfdump –lookup 0x000036d2 –arch armv6 MyApp.app.dSYM

关于 self 和 super 在oc 中 的疑惑 与 分析

Quick Start

这个问题貌似很初级,但很容易让人忽略,me too 。直到在一次面试时被问到,稀里糊涂的回答了下。实在惭愧,

面试一定都是很注重 基础的,不管高级还是初级。

虽然基础好跟基础不好都可以写 代码,网上那么多资料。 区分高低也就是研究的深度和广度。

开始我们的问题:

复制代码
@implementation Son : Father

  • (id)init
    {
    self = [super init];
    if (self)
    {

    }
    return self;
    }
    复制代码
    这段代码 估计很多人都 写烂了,就算没写烂,xcode 自动生成的 我们也看吐了。 好吧,来说说原来,

上来就是 : 这个其实就是 在子类实现初始化前 调用父类的init实现.

这跟没说一样,稀里糊涂的。

首先这个问题,要了解 1, self 是什么 ;super 是什么。2,[ super init] 都做了什么。3,为什么要 self = [super init];

一个一个来:

1,self 是什么 ,super 是什么

在动态方法中,self代表着”对象”

在静态方法中,self代表着”类”

万变不离其宗,记住一句话就行了:self代表着当前方法的调用者

self 和 super 是oc 提供的 两个保留字。 但有根本区别,

self是类的隐藏的参数变量,指向当前调用方法的对象(类也是对象,类对象),另一个隐藏参数是_cmd,代表当前类方法的selector。

super并不是隐藏的参数,它只是一个"编译器指示符"

2, [ super init] 都做了什么

发送消息时

复制代码
Class A
-reposition
{

[self setOrigin:someX :someY];

}
复制代码
A a= [a .. init];

[a reposition]; 方法体中 编译器将

[self setOrigin:someX :someY];
其转换为

objc_msgSend(id self,SEL _cmd, …) 。self -> a
此时 self 指代a 对象,方法从a 对应 类结构的 方法调度表中开始寻找,如果找不到,延继承链往 父类中寻找 。

同样如果 reposition 是类方法, self 指代 A 类对象。

复制代码
Class A
-reposition
{

[super setOrigin:someX :someY];

}
复制代码
[a reposition]; 方法体中编译器将

[super setOrigin:someX :someY];
其转换为

id objc_msgSendSuper(struct objc_super *super, SEL op, …)

第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selector,先看下objc_super这个结构体是什么东西:

struct objc_super {
id receiver;
Class superClass;
};

可以看到这个结构体包含了两个成员,一个是 receiver,这个类似上面 objc_msgSend 的第一个参数 receiver,第二个成员是记录写 super 这个类的父类是什么,拿上面的代码为例,当编译器遇到 A 里

[super setOrigin:someX :someY]
时,开始做这几个事:

>构建 objc_super 的结构体,此时这个结构体的第一个成员变量 receiver 就是 a,和 self 相同。而第二个成员变量 superClass 就是指类 A的 superClass。

>调用 objc_msgSendSuper 的方法,将这个结构体和

setOrigin
的 sel 传递过去。函数里面在做的事情类似这样:从 objc_super 结构体指向的 superClass 的方法列表开始找 setOrigin 的 selector,找到后再以 objc_super->receiver 去调用这个 selector,可能也会使用 objc_msgSend 这个函数,不过此时的第一个参数 theReceiver 就是 objc_super->receiver,第二个参数是从 objc_super->superClass 中找到的 selector

3,为什么要 self = [super init];

符合oc 继承类 初始化规范 super 同样也是这样, [super init] 去self 的super 中调用init super 调用 superSuper 的init 。直到根类 NSObject 中的init ,

根类中init 负责初始化 内存区域 向里面添加 一些必要的属性,返回内存指针, 这样 延着继承链 初始化的内存指针 被从上 到 下 传递,在不同的子类中向块内存添加 子类必要的属性,直到 我们的 A 类中 得到内存指针,赋值给slef 参数, 在if (slef){//添加A 的属性 }

下面来看看这个:

复制代码
@implementation Son : Father

  • (id)init
    {
    self = [super init];
    if (self)
    {
    NSLog(@"%@", NSStringFromClass([self class]));
    NSLog(@"%@", NSStringFromClass([super class]));
    
    }
    return self;
    }
    @end
    复制代码

应该不难分析出 打印结果:

Son
Son

当 发送 class 消息 时不管是 self 还是 super 其消息主体依然是 self ,也就是说 self 和 super 指向的 是同一个对象。只是 查找方法的位置 区别,一个从本类,一个从本类的超类。
一般情况下 class 方法 只有在 根类 NSObject 中定义,极少情况有子类重写 class 方法,
所以 [slef class] 和 [super class] 都是在 根类中 找方法实现, 消息接收主体 又都是 a
如果重写可能会不一样。
自然都打印出 Son

在来一个例子:
复制代码

#import

@interface EngineSuper : NSObject
-(void)printCurrentClass;

@end

#import “EngineSuper.h”

@implementation EngineSuper
-(void)printCurrentClass{

NSLog(@"=EngineSuper=======%@",[self class]);

}
@end

@interface Engine : EngineSuper
-(void)printSupClass;
@end

@implementation Engine

-(void)printSupClass{
[super printCurrentClass];
}

//调用:
Engine *engine = [[Engine alloc]init];
[engine printCurrentClass];//直接调用父类 方法,engine没重载 它

[engine printSupClass];//间接调用父类方法,
复制代码

打印当然都是 :

Engine
Engine

方法体中 self 始终指代 方法的接收者 及对象 engine。,
换成 NSLog(@”=EngineSuper=======%@”,[super class]); 结果也是一样的。

super 就是个障眼法 发,编译器符号, 它可以替换成 [slef class],只不过 方法是从 self 的超类开始 寻找。

二级指针分析

Quick Start

本章中使用的程序是使用Linux的GCC编译出来的,所以汇编代码使用的是AT&T汇编指令,跟windows下使用Intel指令有所不同,详见AT&T与Intel汇编比较。同时,由于我是用的是64位机器,为了方便讲解32位的程序以及防止编译器对代码的优化影响我们对问题的分析,本章所讲解的所有代码编译选项为:gcc -m32 -O0。

概述
Pointers to Pointers:二级指针,我之前把它叫做双指针,比较专业的叫法是二级指针。二级指针是相对一级指针而言的。
二级指针一般用于函数参数传递:

addNode(Type** list);
C语言参数值传递
很多C语言书上,对于参数的值传递都讲解的不是很清楚。对于值传递的理解有助于理解我们理解二级指针。

普通变量的值传递

先看看一段代码:

1 #include
2 #include
3 #include
4
5 void increase(int value)
6 {
7 value = value + 1;
8 }
9
10 int main(int argc, char** argv)
11 {
12 int count = 7;
13 increase(count);
14 printf(“count = %d\n”, count);
15
16 return 0;
17 }
这段代码对应的汇编代码如下:

080483e4 :
80483e4: 55 push %ebp
80483e5: 89 e5 mov %esp,%ebp
80483e7: 83 45 08 01 addl $0x1,0x8(%ebp)
80483eb: 5d pop %ebp
80483ec: c3 ret

080483ed

:
80483ed: 55 push %ebp
80483ee: 89 e5 mov %esp,%ebp
80483f0: 83 e4 f0 and $0xfffffff0,%esp
80483f3: 83 ec 20 sub $0x20,%esp
80483f6: c7 44 24 1c 07 00 00 movl $0x7,0x1c(%esp)
80483fd: 00
80483fe: 8b 44 24 1c mov 0x1c(%esp),%eax
8048402: 89 04 24 mov %eax,(%esp)
8048405: e8 da ff ff ff call 80483e4
//[…]
这段代码执行的结果 count = 7。 我是用gdb调试,打印ESP和count的地址如下:

(gdb) p $esp
$2 = (void ) 0xffffd2b0
(gdb) p &count
$3 = (int
) 0xffffd2cc
main函数内部的汇编如下:

sub $0x20,%esp #esp-0x20,栈向下生长0x20,用来存放局部变量

#在内存单元esp + 0x1c处存放7.

#即count,我上面打印的 $3 - #2 = 0x1c.
movl $0x7,0x1c(%esp)

mov 0x1c(%esp),%eax #将内存单元0x1c即count变量的值copy到EAX寄存器中
mov %eax,(%esp) #copy count变量的内容到当前的ESP寄存器所指向的内存单元
call 80483e4 #调用increase函数
在我的机器上当前运行的ESP指针指向的内存单元是0xffffd2b0,栈向下生长了0x20,则当前栈桢(Stack Frame)的起始地址是0xffffd2b0到0xffffd2d0。count是局部变量,占用的是栈空间,上面gdb打印出来count的地址0xffffd2cc,正好落在main函数的栈桢内。

有一点需要注意的是,在increase调用之前,count变量被copy了一份放在当前ESP所指向内存单元0xffffd2b0,这个count就是为了用来传递参数用的。

接下来看看increase的汇编代码:

push %ebp #ebp压栈,保护上一个栈桢
mov %esp,%ebp #保护ESP
addl $0x1,0x8(%ebp) #将copy出来的那个count变量+1
pop %ebp
ret
increase的汇编代码比较简单,这里只需要解释下addl $0x1,0x8(%ebp)。

由前面一句mov %esp,%ebp可以发现,此时EBP其实是指向栈顶。调用increase之前ESP是0xffffd2b0,由于调用increase需要将下一条IP指令压栈,则ESP = ESP - 0x04 = 0xffffd2ac。在进入increase之后,又执行了一句push %ebp,ESP = 0xffffd2ac - 0x04 = 0xffffd2a8。那么此时栈顶就是0xffffd2a8,EBP的内容就是0xffffd2a8。0x8(%ebp)表示的是EBP + 0x8处的内存单元:0xffffd2a8 + 8 = 0xffffd2b0出的内存单元。

addl $0x1,0x8(%ebp)这句汇编就是在内存单元0xffffd2b0处的内容加+1,最终将加一后的结果继续存放在0xffffd2b0处 。再回顾下,前面0xffffd2b0存放的内容:没错,就是copy出来的count。

看到这里,你会发现,在count传递到increase之后,一直都是在操作copy出来的那个count临时变量,而没有操作真正的count变量。可见,对于普通变量而言,参数的值传递就意味着只是简单的将变量copy了一份传递给函数,普通变量是无法改变外部原始变量的值。

指针的值传递(一级指针)

还是先看代码:

1 #include
2 #include
3 #include
4
5 void increase(int ptr)
6 {
7
ptr = ptr + 1;
8 }
9
10 int main(int argc, char*
argv)
11 {
12 int count = 7;
13 increase(&count);
14 printf(“count = %d\n”, count);
15 return 0;
16 }
这段代码对应的汇编代码如下:

080483e4 :
80483e4: 55 push %ebp
80483e5: 89 e5 mov %esp,%ebp
80483e7: 8b 45 08 mov 0x8(%ebp),%eax
80483ea: 8b 00 mov (%eax),%eax
80483ec: 8d 50 01 lea 0x1(%eax),%edx
80483ef: 8b 45 08 mov 0x8(%ebp),%eax
80483f2: 89 10 mov %edx,(%eax)
80483f4: 5d pop %ebp
80483f5: c3 ret

080483f6

:
80483f6: 55 push %ebp
80483f7: 89 e5 mov %esp,%ebp
80483f9: 83 e4 f0 and $0xfffffff0,%esp
80483fc: 83 ec 20 sub $0x20,%esp
80483ff: c7 44 24 1c 07 00 00 movl $0x7,0x1c(%esp)
8048406: 00
8048407: 8d 44 24 1c lea 0x1c(%esp),%eax
804840b: 89 04 24 mov %eax,(%esp)
804840e: e8 d1 ff ff ff call 80483e4
// […]
这段代码的执行结果是8。
这段代码跟上一段代码的唯一区别是将count的地址传递给increase函数了。

main函数的汇编代码

push %ebp
mov %esp,%ebp
and $0xfffffff0,%esp
sub $0x20,%esp
movl $0x7,0x1c(%esp)

lea 0x1c(%esp),%eax #将count变量的地址赋值给EAX
mov %eax,(%esp)
call 80483e4
跟前面的main函数的唯一区别是lea 0x1c(%esp),%eax

看懂这段代码首先要补习下lea指令。lea指令跟mov指令很相似,区别在于lea类似于C语言中的&取地址。那么lea操作也只是简单的针对地址做加法而已,而不会针对这个地址单元取操作数。

那么这代码在调用increase函数之前,当前ESP所指向的内存单元的值是count变量的地址。而上一段代码在调用increase之前,当前ESP所指向的内存单元的值是count临时变量的值。

我们再来看看increase函数的汇编代码

push %ebp
mov %esp,%ebp
mov 0x8(%ebp),%eax #前面已经讲过了

取出EAX所指向的内存单元的值赋值给EAX

也就是说执行此句话之后,EAX的内容是

count变量的值,而不是地址。

mov (%eax),%eax
lea 0x1(%eax),%edx #将EAX的内容加一,将加一后的结果存放到EDX
mov 0x8(%ebp),%eax #重新将count变量的地址赋值给EAX

#将EDX的内容存放到EAX所指向的内存单元

#就是将加一后的结果重新赋值给main函数里的count变量
mov %edx,(%eax)
pop %ebp
ret
理解这段汇编代码,需要记住一点,在调用increase之前,栈顶ESP所指向的内存单元的值是count变量的地址。之后,经过压栈IP,进入increase函数,再压栈EBP。则0x8(%ebp),EBP + 0x8表示的就是在调用increase前,栈顶所指向的内存单元,里面存放的是count变量的地址。也就是说mov 0x8(%ebp),%eax之后,EAX的内容就是count变量的地址。紧接着mov (%eax),%eax是现将EAX指向的内存单元的内容取出来存放到EAX中,此时EAX寄存器的内容已经不是地址了,而直接是count变量的值。然后对其做加一操作,存放到EDX当中。

下面是最关键的两句话:

mov 0x8(%ebp),%eax
mov %edx,(%eax)
由于EBP + 0x8里面放的是count变量的地址,mov 0x8(%ebp),%eax之后,EAX中存放的就是count变量的地址。

EDX存放的是前面计算的结果,最后mov %edx,(%eax),将前面计算的结果重新存放到EAX所指向的内存单元,即重新给count变量赋值。

看到这里,你会发现,函数参数值传递,对于指针变量来说,也只是仅仅传递了一个内存地址,然后对这个内存地址进行操作。由于内存地址是进程级别的,所以,在函数内部 ,对地址所指向内容的修改,是可以带到函数外部的,是可以操作到函数外面的源变量的。

二级指针
我们改造下上面的代码

1 #include
2 #include
3 #include
4 void increase(int ptr)
5 {
6
ptr = ptr + 1;
7 ptr = NULL;
8 }
9
10 int main(int argc, char** argv)
11 {
12 int count = 7;
13 int
countPtr = &count;
14 increase(countPtr);
15 printf(“count = %d\n”, count);
16 printf(“countPtr = %p\n”, countPtr);
17 return 0;
18 }
运行结果,count = 8,而countPtr则不是NULL。

运用前面的理论,其实很容易分析出问题。一级指针变量,也是一个普通变量,只不过这变量的值是一个内存单元的地址而已。countPtr在传递给increase之前,被copy到一个临时变量中,这个临时变量的值是一个地址,可以改变这个地址所在内存单元的值,但是无法改变外部的countPtr。

从这个结果可以得出一个结论:一级指针作为参数传递,可以改变外部变量的值,即一级指针所指向的内容,但是却无法改变指针本身(如countPtr)。

有了上面的理解基础,其实对于理解二级指针已经很容易了。

对于指针操作,有两个概念:

引用:对应于C语言中的&取地址操作
Reference

解引用:在C语言中,对应于->操作。
Dereference operator

对于一个普通变量,引用操作,得到的是一级指针。一级指针传递到函数内部,虽然这个一级指针的值会copy一份到临时变量,但是这个临时变量的内容是一个指针,通过->解引用一个地址可以修改该地址所指向的内存单元的值。

Alt Text
对于一个一级指针,引用操作,得到一个二级指针。相反,对于一个二级指针解引用得到一级指针,对于一个一级指针解引用得到原始变量。一级指针和二级指针的值都是指向一个内存单元,一级指针指向的内存单元存放的是源变量的值,二级指针指向的内存单元存放的是一级指针的地址。

二级指针一般用在需要修改函数外部指针的情况。因为函数外部的指针变量,只有通过二级指针解引用得到外部指针变量在内存单元的地址,修改这个地址所指向的内容即可。

我们针对上面的代码继续做修改

1 #include
2 #include
3 #include
4 void increase(int ptr)
5 {
6
ptr = ptr + 1;
7 *ptr = NULL;
8 }
9
10 int main(int argc, char
argv)
11 {
12 int count = 7;
13 int* countPtr = &count;
14 increase(&countPtr);
15
16 printf(“count = %d\n”, count);
17 printf(“countPtr = %p\n”, countPtr);
18 return 0;
19 }
这段代码,运行结果count = 8, countPtr = NULL;

总结
首先,指针变量,它也是一个变量,在内存单元中也要占用内存空间。一级指针变量指向的内容是普通变量的值,二级指针变量指向的内容是一级指针变量的地址。

title:oc runtime system

Quick Start

与Runtime交互
Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

Objective-C源代码
大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的id和SEL都是啥)
NSObject的方法
Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
Runtime的函数
Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

runtime 机制

Quick Start

对于runtime机制,在网上找到的资料大概就是怎么去用这些东西,以及查看runtime.h头文件中的实现,当然这确实是一种很好的学习方法,但是,其实我们还是不会知道runtime底层编译成C++语言之后做了什么?
查到一个大牛给资料,顿时对runtime有了一定认识!

我们随便写一个小程序,代码如下:
person类头文件如下,

#import
@interface Person : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;

@end

main.m文件如下

int main(int argc, const char * argv[])
{

Person *p = [[Person alloc] init];

NSString *str = @”zhangsan”;

p.name = str;
// p.name 等价于
[p setName:str];

p.age = 20;

return 0;
}

然后我们打开终端,在命令行找到cd到文件目录,然后中输入:

clang -rewrite-objc main.m
命令可以将main.m编译成C++的代码,改成不同的文件名,就会生成不同的c++代码
这是就生成了main.cpp这个c++文件,打开文件代码
查看该main.cpp最底下的main函数,
这样我们就可以看到底层具体实现的方式!

这时,我们就需要知道这些方法:
objc_msgSend 可以给对象发送消息
objc_getClass(“Person”) 可以获取到指定名称的对象
sel_registerName(“alloc”) 可以调用到对象的方法

通过查看,c++代码,我们得出结论:
使用objc_msgSend函数,给objc_getClass函数实例化的对象发送sel_registerName获取到的方法
这么一个消息
代码是给人看的,顺带让机器实现功能。日常的程序开发过程中,要少用runtime,

那什么时候会使用runtime呢?
runtime应用的时机:
1> 当需要非常高的性能开发时,使用runtime,注释:oc的代码已经无法满足性能需求
2> 当我们对系统内部的实现很好奇的时候,可以用clang反编译成c++去看底层的实现机制!

Java关键字

Java关键字及其作用
一、 关键字总览:
访问控制

private

protected

public

类,方法和变量修饰符

abstract

class

extends

final

implements

interface

native

new

static

strictfp

synchronized

transient

volatile

程序控制

break

continue

return

do

while

if

else

for

instanceof

switch

case

default

错误处理

try

cathc

throw

throws

包相关

import

package

基本类型

boolean

byte

char

double

float

int

long

short

null

true

false

变量引用

super

this

void

保留字

goto

const

二、 详细解释

  1. 访问控制
    1) private 私有的
    private 关键字是访问控制修饰符,可以应用于类、方法或字段(在类中声明的变量)。 只能在声明 private(内部)类、方法或字段的类中引用这些类、方法或字段。在类的外部或者对于子类而言,它们是不可见的。 所有类成员的默认访问范围都是 package 访问,也就是说,除非存在特定的访问控制修饰符,否则,可以从同一个包中的任何类访问类成员。

Objective-C 底层分析

过去的几年中涌现了大量的Objective-C开发者。有些是从动态语言转过来的,比如Ruby或Python,有些是从强类型语言转过来的,如 Java或C#,当然也有直接以Objective-C作为入门语言的。也就是说有很大一部分开发者都没有使用Objective-C太长时间。当你接触 一门新语言时,更多地会关注基础知识,如语法和特性等。但通常有一些更高级的,更鲜为人知又有强大功能的特性等待你去开拓。

这篇文章主要是来领略下Objective-C的运行时(runtime),同时解释是什么让Objective-C如此动态,然后感受下这些动态化的技术细节。希望这回让你对Objective-C和Cocoa是如何运行的有更好的了解。
The Runtime
Objective-C是一门简单的语言,95%是C。只是在语言层面上加了些关键字和语法。真正让Objective-C如此强大的是它的运行时。它很小但却很强大。它的核心是消息分发。
Messages
如果你是从动态语言如Ruby或Python转过来的,可能知道什么是消息,可以直接跳过进入下一节。那些从其他语言转过来的,继续看。
执行一个方法,有些语言,编译器会执行一些额外的优化和错误检查,因为调用关系很直接也很明显。但对于消息分发来说,就不那么明显了。在发消息前不 必知道某个对象是否能够处理消息。你把消息发给它,它可能会处理,也可能转给其他的Object来处理。一个消息不必对应一个方法,一个对象可能实现一个 方法来处理多条消息。
在Objective-C中,消息是通过objc_msgSend()这个runtime方法及相近的方法来实现的。这个方法需要一个target,selector,还有一些参数。理论上来说,编译器只是把消息分发变成objc_msgSend来执行。比如下面这两行代码是等价的。
[array insertObject:foo atIndex:5];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);Objects, Classes, MetaClasses
大多数面向对象的语言里有 classes 和 objects 的概念。Objects通过Classes生成。但是在Objective-C中,classes本身也是objects(译者注:这点跟python很 像),也可以处理消息,这也是为什么会有类方法和实例方法。具体来说,Objective-C中的Object是一个结构体(struct),第一个成员 是isa,指向自己的class。这是在objc/objc.h中定义的。