Objective-C的对象模型与运行时

本人blog的前一篇文章从Objective-C的消息发送角度整理了运行时的相关内容,本篇文章我们就来从数据结构方面分析下为什么Objective-C可以拥有消息发送中的这些特点,进一步剖析下Objective-C的运行时。

0. Objective-C与C

从Objective-C的名字上我们就能看出来它和C语言有着很密切的关系,是“面向对象的C语言”。Objective-C诞生于上世纪80年代,那个时候面向对象的理念已经得到了初步发展,C语言当时也已经是很成熟的语言。和C++类似,Objective-C也在C语言的基础上,增添了面向对象的特性。同是在1983年前后出现,Objective-C和C++在对C语言的面向对象特性扩展上,走出了不同的道路。

Objective-C语言的实现用到了一个用C语言写的运行时系统,也就是一个动态链接库——libobjc.A.dylib。这个动态库里面提供了Objective-C语言所需的各种动态特性,包括支持面向对象的一些特性。而Objective-C的编译器可以以C编译器为基础,对Objective-C的语法做“预处理”扩展(GCC里的Objective-C编译器就是这么做的)。

所以可以说Objective-C是以C语言为基础,通过引入特有的运行时库,形成一门支持面向对象特性的语言。

1. Objective-C目标文件中的运行时信息

前一篇文章中,我们通过一段示例代码演示了运行时对面向对象中多态特性的支持。一个消息发送语句会根据运行时的信息判断,具体应该调用哪个函数实现(比如区分父类还是子类)。而这些运行时所需要的信息最终又是开发人员在写代码时就添加进去的,其中蕴含着特定的逻辑。

其实,可以说不仅仅是Objective-C,绝大多数面向对象语言都是类似的。想要支持运行时多态,代码中一定包含有运行时所需的判断数据,而且经过编译器编译过后一定会保留下来。只有这样,机器在执行时才能有所依据。

像Java的JVM,在运行时为Java程序提供了强大的支持,而Java的运行时信息都在.class文件中按规范格式保留下来。编译后的C++代码也一样保有RTTI。

不像Java的虚拟机那样重,能够支持python等其它语言生成的class。Objective-C的运行时针对Objective-C语言,更加短小精悍、简单实用。而Objective-C代码中的运行时信息都保留在编译过后的目标文件中(.o)。

实用Objective-C的编译器编译.m文件之后,我们会看到生成有.o目标文件,目标文件中包含头部、加载指令和各个段。其中有一个segment专门负责保留Objective-C的运行时信息。

比如,实用otool,我们可以看到其中的内容:

Objective-C目标文件中的运行时信息

Objective-C目标文件中的运行时信息

如上图中所示,这个.o目标文件中包含了一个类(class)和一个元类(meta class)的信息。这其中包括实例的大小、类名、父类、方法列表即使地址、protocal起始地址等。

如果,你和我一样,之前做过Java,并且对Java的class文件格式很感兴趣,那么现在你会发现,这个目标文件中包含的Objective-C运行时信息和class中的数据内容极为相似。

是的,技术方案是互通的,是相似的,相信其他很多面向对象的语言也有类似的方式。

2. 对象模型

刚刚提到了类和元类,这些是什么? 在Objective-C中一个对象又是怎样的实现? 什么是对象? 相信读完本段对Objective-C对象模型的介绍,如上问题就会自然解开了。

首先我们相信,无论是类还是对象,在最终的实现中都是一种数据结构,这点应该没什么好质疑的。

我们来看看Objective-C的对象是个什么东西?

typedef struct objc_class *Class;
typedef struct objc_object {
    Class isa;
} *id;

从objc.h中的如上代码来看,Objective-C的对象就是一个包含isa指针的数据结构,而isa又是一个Class类型的,Class则是一个名为objc_class的数据结构定义。

再看runtime.h中对objc_class的定义:

struct objc_class {
    Class isa;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *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;

其中也是一个包含isa的结构。咦!那objc_object和objc_class不就一样了?实际上看到这里,的确就是这样。

当然,一个实际的类,里面还会包含各个变量成员,所以类定义好了,category也不能直接增加变量存储。而这个isa就指向这个对象的实际类(isa指向一个“类对象”)。正是因为这样,运行时一个消息发送才知道具体应该调用哪个方法实现。同时,一个类又包含了这个类的所有信息(在“类对象”结构中记载)。这个“类对象”的isa指向元类(meta class)对象。“类对象”中有方法列表,这里面的方法是实例方法(减号“-”方法),而“元类对象”中的方法列表则是类方法(加号“+”方法)。

我们来看张对象的图:

Objective-C类实例结构图

Objective-C类实例结构图(引自“帝国神农”的blog)

有没有觉得这个结构很熟悉? 没错,其实debug的时候都能看到,只是不是以图像的形式展现出来而已。

而关于“类”和“元类”,我们再看另一张图:

Objective-C对象模型结构图

Objective-C对象模型结构关系图

看完上面这张Objective-C对象模型的关系图,似乎一切就都清楚了!

就整理到此,希望能够帮助各位朋友们读懂Objective-C运行时中的奥秘。

参考:

发表在 iOS, Objective-C, 开发, 计算机技术 | 标签为 , , , , , | 9 条评论

从消息发送(方法调用)看Objective-C的运行时

Objective-C是一门动态语言,可以在运行的时候动态决定调用哪个方法实现,甚至增加、替换方法的具体实现,而这些都归功于Objective-C的运行时(runtime)系统。本篇文章,我们就从消息发送的角度来看下Objective-C的运行时。

0. 决定方法调用的动态性

Objective-C语言是一门面向对象编程语言,而面向对象的一个基本特征就是多态。在一个复杂的类的继承层次结构中,子类可以和父类具有同名的方法(override),父类的引用也可以接受子类对象。而在这种情况下,调用父类引用的方法(或者说发送某个消息),那么如果这个方法在父类和子类中的实现逻辑不同,哪种实现会被执行呢,答案自然应该是子类的实现逻辑被调用执行。这是面向对象语言的基本特性之一。

C作为一门非面向对象语言,肯定是不支持这些的,而建立在C基础之上的Objective-C通过运行时库做到了这点。比如有两个类,ParentClass和它的子类SonClass,具有同名不同实现的无参实例方法(“-”)doSomething,instance是一个ParentClass*类型引用,但却被指向了一个SonClass的实际对象。那么调用[instance doSomething]; 毫无疑问会执行SonClass里的逻辑。

这怎么做到?这正是通过Objective-C运行时对消息发送的分派机制。赘言无益,看看下面一段代码就明白了:

void funcA()
{
   printf("hello world!\n");
}

void funcB()
{
   printf("world hello!\n");
}

void receiveMessage()
{
   void (*function)();
   if ( shouldFunctionAorB )
   {
      function = &funcA;
   }
   else
   {
      function = &funcB;
   }
   (*function)();
}

这是一段C语言代码,使用了函数指针,函数主体逻辑在receiveMessage函数里,它通过运行时shouldFunctionAorB变量的状态选择最终执行funcA还是funcB。

虽然本人未看过Objective-C运行时库的源代码,但相信其实现的方式与此并无太大区别,原理就是如此。而决定运行时执行哪个方法实现的条件可能会比较复杂,但runtime肯定是可以得到并以此决断的。

1. SEL和IMP

使用过UIControl的朋友应该知道addTarget:action:forControlEvents:这个方法,里面的action参数通常用到一个@selector(),而这个语句的结果就是得到了一个SEL变量。SEL变量我们就叫做一个selector。Selector其实很好理解,就是在发送消息/方法调用时标识是哪个消息(方法)的东西。这个通常都是通过方法全名得来的,就比如上面UIControl的(addTarget:action:forControlEvents:)。

IMP实际上就是具体的一个方法逻辑实现(implementation,貌似IMP就是这么来的)。这个和上面代码示例中的C语言函数指针概念相似。到funcA的指针和到funcB的指针都是IMP变量。

Obejctive-C的运行时系统提供的消息的分派机制把SEL和IMP关联起来。但每次消息发送/方法调用都做一遍查找显然是很麻烦很低效的,所以在Objective-C的runtime这里一定是有缓存机制的, 使得对每个类特定selector对应的IMP可以很快找到。

当然,即使用最好的数据结构和最快的查找算法,和直接执行C函数调用的静态绑定方式相比,也一定有性能损失。但比起Objective-C运行时为开发者提供的诸多动态特性相比,这些都是值得的。

2. objc_msgSend

0中的代码例子中阐述了Objective-C中消息发送的原理,而1中也解释了SEL和IMP的概念。那么当这一切都清楚了的时候,实际消息发送的时候是怎么操作的呢?那就是通过objc_msgSend这一系列的运行时C函数调用来做到的了。

在苹果官方文档《Objective-C Runtime Programming Guide》中提出和Objective-C运行时交互有三种方式,其中最底层的一种方式就是直接使用runtime的函数。我们下面就来看下和发送消息直接相关的几个函数:

  • objc_msgSend
  • objc_msgSend_fpret
  • objc_msgSend_stret
  • objc_msgSendSuper
  • objc_msgSendSuper_stret

我们看到他们都是以objc_msgSend开头的,也就是Objective-C“消息发送”的意思,我们就来看最基础的,也就是第一个。

id objc_msgSend ( id self, SEL op, ... );

我们看到后面有self、op和变长参数列表。我们知道对于不同的两个Objective-C的类对象,执行消息发送结果可能是不同的,那么第一个参数id类型的参数self就是用来标识不同对象的,而SEL的op就是标识发送哪个消息/调用哪个方法的selector,后面的变参我们其实可以猜想到,就是selector对应方法的实际参数。

而具体找IMP的过程显然被包装在objc_msgSend里面,或者说selector和IMP的对应关系被运行时系统记录了下来,无需objc_msgSend的调用者直接关注。

这样看来,这个objc_msgSend实际上就相当于0中示例代码的receiveMessage函数了。

上面列表中objc_msgSend一族的其它几个函数其实功能上是差不多的:

  • 带有ret结尾的标识返回值在某些情况下可以特殊处理(比如使用处理器的寄存器存储而非开辟栈空间),以进行优化。
  • 带有super的标识的回去找id参数的父类。关于怎么找一个Objective-C的当前类和父类,本小站后面的技术文章会找机会解释。

3. Method swizzling

上问提到了SEL和IMP的对应关系,那么这个能不能改呢?了解运行时机制除了知道objc_msgSend原理和怎么调用外还有什么意义?Method swizzling就是两个问题的一个很好的答案。

Method swizzling比较直接的翻译就是方法(实现)交换(“搅合”)。下面是一个简单的例子(源自《Effective Objective-C》):

Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

这样,NSString之后的lowercaseString和uppercaseString就是相反的效果了!

方法实现的交换可以有很多应用场景,尤其在调试系统库等领域。

4. 消息转发

我不知道阅读本文的朋友们在开发调试时有没有遇到过“unrecognized selector”这个异常。这个错误提示告诉我们在程序运行时调用的方法没找到对应的实现,通常一个app就这样直接crash了。而其实这个情况系统是做过处理尝试的,最终这个异常也是系统库中NSObject的方法实现抛出来的。

这一段我们整理下Objective-C运行时中的消息转发机制,然后我们就明白上面这个异常提示是怎么出来的了。

消息转发通常是在Objective-C运行时系统找不到一个selector对应的实现时,这时候系统会回过来询问这个对象所属的类应该怎么做,主要分为几步:

  • 要不要对此次消息调用(实例方法/类方法)做动态解析和执行处理。这个我们可以通过class_addMethod函数,给这个类一个IMP,这样就可以去执行了。很多@dynamic的标记就需要这么配合来做。
  • 不做动态解析,那么是否转移消息给别的对象,转给谁。
  • 不转给别的对象,那么这次消息发送通过调用哪个Invocation对象来处理。

如上过程可参看如下取自《Effective Objective-C》的截图:

Objective-C运行时的消息转发

Objective-C运行时的消息转发

实际上,所有类的forwardInvocation方法都从NSObject那里继承到了,而其会调用:

- (void)doesNotRecognizeSelector:(SEL)aSelector;

除了这些,消息转发很强大,用好了大有作为,比如可以做到类似C++中多继承的一些效果。

本文就整理到这,下面写本文时参看的一个有意思的参考:

http://www.cocoawithlove.com/2008/02/imp-of-current-method.html

发表在 iOS, Objective-C, 开发, 计算机技术 | 标签为 , , , , , | 6 条评论

Objective-C的AssociatedObject

Objective-C是一个具有动态特性的语言,我们可以为一个已有的类增添一个Category,在其中为已有类增加一些新东西。但需要注意的是,如果这个Category不和此类的原始定义再一起,那么这个类只能增加方法,而不能增加成员变量。这就意味着,即使我们在这个Category中写了@property,系统也不会为我们生成变量存储。但如果我们真得非要这么做,有没有办法呢?有,那就是AssociatedObject。这篇文章就介绍下Objective-C里的Associated Object。

0. 基本概念

Associated Object翻译过来就是关联对象。顾名思义,它就是指一个关联的对象,把一个key-object对和一个已有的特定对象关联起来。

比如有一个对象A,和另一个对象B,我们可以找一个合适的key,让他们之间产生关系。在已知A的情况下,我们通过key就能找到和A关联起来的B对象。

我们来看下Objective-C运行时库提供的几个方法:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
id objc_getAssociatedObject(id object, const void *key)

看到这两个方法就一切大白了。

这其中还有一个objc_AssociationPolicy类型的参数,这个是与内存策略有关,就好比在@property后面的属性说明assign、retain、copy等。其中包括了如下值:

enum {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied.
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

1. Category的使用

既然了解了这个道理,我们不妨回头看看文章开头的问题。假设现在已有class A,现在要对A类做扩展,我可以写一个A的Category,增加一个property,比如叫name。

@interface A(MyCategory)
@property (nonatomic,retain) NSString * name;
@end

同时在实现中:

static void *MyNameKey = "MyNameKey";
@implementation A(MyCategory)
-(void)setName:(NSString*)name
{
   objc_setAssociatedObject(self, MyNameKey, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString*)name
{
   return objc_getAssociatedObject(self, MyNameKey);
}
@end

这样就OK了。

(当然,在《Effective Objective-C》中,作者不建议这样写,避免造成混淆和错误,带来不必要的麻烦)

2. UIAlertView的使用

与上述Category类似,在使用UIAlertView和它的delegate时,Associated Object也能较好的发挥作用。

尤其是在有多个UIAlertView的时候,我们可以把UIAlertView作为原始对象,把和alertView特定按钮的条件作为key,对应要执行的内容封装成一个block作为object,设置关联对象。

这样在delegate的clickedButtonAtIndex回调时,可以根据条件直接找到对应的block代码块,进行执行。

具体实现就不在此列出了。

关于Associated Object就简要介绍到这,如果有更有意思的玩法,欢迎补充进来。

发表在 iOS, Objective-C, 开发, 计算机技术 | 标签为 , , , | 2 条评论

Objective-C和Swift中的对象初始化

Objective-C作为一门面向对象语言,我们在开发时所做的所有事情都是在与对象打交道。那么一个对象,也就是一个Object是怎么产生的呢?通常我们要为其分配内存,同时还要进行初始化。说起Objective-C的初始化,这当中的逻辑细说起来也有很多要点需要考虑的,不清楚这些就很可能犯低级错误,这篇文章就和新语言Swift一起,来整理下对象的初始化过程。

0. Objective-C初始化方法

本人之前是做Java开发的,Java中有用类名作为方法名的方法,可以带参数也可以不带参数,这就叫构造方法。在Java对象实例化时,直接一个new操作就都OK了,Java系统帮你把对象内存在堆上分配好,并初始化各项数据。

而在Objective-C中就有所不同了,为了最大程度让开发者控制整个分配内存和初始化的流程,苹果将其一分为二。即为对象分配内存用alloc,初始化使用init。

比如我们看下NSObject这个类:

  • alloc方法根据对象包含数据结构的需要,分配内存,此后设置isa(告诉系统这是什么类的对象,后面有时间会专门写篇文章讲这个),然后将必要是数据结构都设置为0
  • init方法,直接返回self对象

如果我们自己的类中包含了业务逻辑相关的数据,则需要在自己的init方法中,将这些变量初始化成可用的值。如果无法完成初始化,则应返回nil。

所以,我们通常将alloc和init的方法连起来调用,只有init过之后的才是可靠的,因为alloc的返回结果可能在init后发生变化,甚至不可用。只有init执行返回的对象才是可用的对象。

一般情况下不要这样使用:

id anObject = [SomeClass alloc];
[anObject init];
[anObject someOtherMessage];

因为init执行的返回结果,可能已经与anObject本身不同。

1. Objective-C的指定初始化器

面向对象在很大程度上是为了复用而设计,而为了最大程度的代码复用,也为了降低代码维护成本,更可控,苹果在Objective-C中阐述了一个概念——Designated Initializer,指定初始化器,或者叫指定初始化方法。

再提一提Java,Java在5以后的API中有个ThreadPoolExecutor类,这个类有4个构造方法,参数各不相同,最多的一个参数有7个。而其它3个方法,在实现上都是通过this()调用更多参数的方法,空余的参数用默认值补上。那么7个参数的构造方法在这里就可以认为是指定初始化器。

初始化器索要完成的任务是实质性的,比如根据用户给出的自定义参数初始化对象成员变量值,在有父类的情况下,负责调用父类(指定)初始化器。指定初始化器一般是类层次间的唯一初始化出口,也通常是参数最多的那个initXXX方法。

一个类通常只有一个指定初始化器,但在某些特殊情况,比如通过反序列化重建对象的时候,可能会有多个。苹果iOS系统库中的类的指定初始化器都会在文档中列出。

2. 继承和初始化链

当涉及到继承关系的时候,情况变得更加复杂一些(所以一个业界常问的问题就是在设计时继承优先还是组合优先)。

在Java中,这个通常不用自己操心,必要的时候调用super()就好。

在Objective-C中,首先,每个类的层次结构负责给自己定义的成员变量/属性赋予初始值。而一个子类对象只有在父类对象得到初始化时,再进行自定义初始化才有意义。所以,一个类要在init方法中进行父类初始化,而且需要手动调用父类初始化方法。如下是一个常规的初始化方法实现:

- (instancetype)init
{
    self = [super init];
    if (self) {
        ;
    }
    return self;
}

就这样,有继承关系的各个层次间的类构成了一道完美的初始化器链。

除此之外,继承还带来了额外两个问题(至少在Objective-C中是都存在的):

  • 为了充分满足子类的指定初始化器要求,如果子类初始化器和任何上层类的指定初始化方法名字有所不同(一般由于子类的特性,都会在参数上有所不同),必须在子类重写各层父类初始化方法,使其调用当前类指定初始化器,构成完整的初始化链
  • 子类的指定初始化器只能调用父类的指定初始化器,至于原因,我这里先不写明,让大家思考下(其实和其他语言的考虑一样)

Swift作为新生语言,作为Objective-C的后继者,自然应当有所改进,接下来我们一起看下。

3. Swift中的对象初始化

Swift是WWDC2014苹果推出的新语言,旨在降低语言门槛,提高开发效率。

在Swift中,对象初始化调用更像Java了,无需分别调用alloc和init来拿到对象,直接向下面这样使用就可以了:

var f = Fahrenheit()。

同时支持默认情况下property的初始化。init也没有返回值,无需alloc和init联合调用取最后返回值了。在init中还可改变常量。

初始化比较复杂的点还在于初始化方法的委派调用(delegation),对于非class的结构,比如struct的初始化,没有继承,只能通过调用self.init()的方式进行委派调用。而最复杂的委派调用,依然还是在有继承情况下:

  • 最基本的,非指定初始化器和指定初始化器构成的调用顺序链和Objective-C基本一致。非指定初始化方法和制定初始化方法写法略有不同,非指定初始化方法有一个convenience关键字。
  • 两阶段初始化,第一阶段完成自身变量有初始化值并保证父类的也都完成变量值初始化,逐层向上,第二阶段顺序反过来进一步在子类中自定义对象状态(此时可以使用self)。
  • 对于父类的初始化方法,按两种规则进行继承,其它情况下都不自动继承。这个基本上很好地解决了本文上一个section中的两个问题。规则一,如果本类中新增的属性成员有默认初始化值,而且没有定义制定初始化方法,那么将自动继承superclass中所有的指定初始化器;规则二,无论是通过自己实现还是通过规则一继承,如果当前类中拥有了所有父类实现过的指定初始化器,则自动继承superclass中所有的非指定初始化器。一个实例见下图。
Swift中类初始化器的自动继承

Swift中类初始化器的自动继承

关于Objective-C、Swift初始化的内容整理到这,欢迎各位补充交流。

发表在 iOS, Objective-C, 开发, 计算机技术 | 标签为 , , , , | 2 条评论

Tomcat7源码分析(上)——启动过程和类加载器

上篇Java的文章中我将Java Servlet的笔记整理出来,其中简单介绍了Servlet的技术和相关概念。这篇文章我们来以Tomcat的源码为例,看看Servlet的容器怎么来实现。当然,这也是我本人较早时候的一个源码分析笔记,使用的Tomcat版本是7.0。

0. Tomcat简介

Tomcat,全名Apache Tomcat,最初是由Sun发起,后来捐赠给ASF,是Apache Jakarta下的一个子项目。Tomcat是对Servlet API定义的容器的一个完整实现,同时它也不仅仅是一个容器,也完全可以当做一个纯Java实现的HTTP服务器来使用。按照维基百科最早的记载,是在1999年发布了3.0.x版本(合并此前Sun维护的代码),可以说是一个比较早的Servlet容器实现。最初作为Sun对Servlet规范的一个参考实现,Tomcat完整性很好,最重要的,Tomcat是开源的,我们本文以Tomcat为例,进行分析。

当然,其它优秀的servlet容器还有很多,如Jetty等。

1. 启动过程

不管是操作系统,还是应用程序,都要从无到有,有一个循序渐进的启动过程。Tomcat也不例外,也是一切从头开始。嵌入其它应用,通过代码启动在新版本的Tomcat中也是支持的,但本文这里已最常见的命令行启动为例开始介绍。

我们打开Tomcat的压缩包,里面有startup.sh脚本和为了支持Windows环境的.bat文件,Tomcat启停的奥义就在这里。startup.sh里有一句:

exec "$PRGDIR"/"$EXECUTABLE" start "$@"

其中的$EXECUTABLE是在上面赋值的catalina.sh,也就是说会去执行catalina.sh,而且紧随其后的参数是start,我们继续看catalina.sh这个脚本,里面有这么一行开启的段落:

elif [ "$1" = "start" ] ; then

最终的执行语句是:

eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start \
      >> "$CATALINA_OUT" 2>&1 "&"

而$_RUNJAVA就是系统环境下的的java,忽略掉中间的参数不管,我们可以看到启动的Java类是org.apache.catalina.startup.Bootstrap。

_RUNJAVA="$JRE_HOME"/bin/java

而这个org.apache.catalina.startup.Bootstrap类里面有一个main方法,这和我们简单直接写的一个HelloWorld类编译执行的原理是一样的。 

这里,我们可以有一个结论,在读任何系统的源码时,都将有一个入口,把握住了入口就把握住了一切。而任何用java运行起来的应用最终也都会有一个public class和一个main,这是绝对的,虽然servlet开发不需关注,只要针对API编程即可,但即使这样最终还是通过servlet的container来满足这一点。

也就是这样,Tomcat启动了。

2. Bootstrap类

上文书说到org.apache.catalina.startup.Bootstrapmain方法得到执行。找到对应的tomcat源代码,我们看到其中可以分为2大块:静态对象daemon的创建和初始化、根据命令行参数对daemon调用对应的方法。

先看第一块,从daemon的声明我们可以看出其就是一个Bootstrap的对象,在其为null的时候需要新创建一个,并且执行init()方法,初始化后将其赋值给daemon。我们这里更进一步,看看Bootstrap的这个init()方法到底做了哪些事情。

    public void init()
        throws Exception
    {
        // Set Catalina path
        setCatalinaHome();
        setCatalinaBase();
        initClassLoaders();
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        Class<?> startupClass =
            catalinaLoader.loadClass
            ("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.newInstance();

        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;
    }

而这个方法,又可以分为两大部分:

  • 前5条语句,用来初始化catalina类加载器相关环境的变量,然后初始化各个类加载器对象,包括commonLoader、sharedLoader,其中最重要的就是catalinaLoader,将其设置为Bootstrap的一个属性值。
  • 从第六条语句开始,使用前面已经构件好的catalinaLoader加载tomcat最核心的对象,那就是org.apache.catalina.startup.Catalina类的对象catalinaDaemon,并以反射的方式调用其setParentClassLoader方法,把sharedLoader作为参数传入。

我们看到这个过程中,Bootstrap类持有的几个对象(静态的daemon、非静态的catalinaDaemon、commonLoader、sharedLoader、catalinaLoader)都得到了创建和必要的初始化。

那么接下来我们看main方法的后半部分。比如刚刚启动,我们得到的命令行参数是start,那么会执行如下这段代码:

            else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
            }

会分别调用Bootstrap类的daemon对象的setAwait()、load()和start()三个方法。这三个方法我们略微深入一些,发现是从Bootstrap的方法到Catalina方法的一个调用,而且中间都是用了反射方式。至于这3个方法最终具体做了什么,我们这里先不细说,后面会分块做详细整理,不过可以简单了解下,其中:

  • setAwait()是设置了Catalina对象的一个属性值,其作用是告诉服务器启动后保持运行状态,并开启特定端口监听后续发来的指令,直到收到SHUTDOWN指令,做关闭服务器处理。
  • load()则是加载和初始化。对整个Tomcat服务器相关的配置文件进行加载和解析处理,并对Tomcat的各个组件进行初始化配置操作。
  • start(),这个其实不用多说,就是正式启动Catalina,或者说启动了Tomcat服务器的核心工作。

以此为例,对于Bootstrap的其它命令,比如stop等,本文就不想详细叙述了,感兴趣的可以按照这个路子去查看源代码。

3. Tomcat中的类加载器

了解Java和JVM的各位朋友一定知道,现代的JVM中通常有三层默认的类加载器,分别是bootstrap类加载器、扩展类加载器和系统类加载器。这三者每两者间都是父子关系,即前者是后者的父亲或者双亲类加载器,并由此构建了一个“双亲委派关系”,或叫“代理”关系。关于JVM和Java类加载器,我本人争取在后面有机会单独给大家介绍,这里先简单提一下,感兴趣的可以先参考这篇文章:http://www.ibm.com/developerworks/cn/java/j-lo-classloader

除了Java环境自身的三层类加载器,前面提到的,在Tomcat中主要有commonLoader、catalinaLoader、sharedLoader这几个类加载器。细看源码,实际上这几个loader都是Tomcat中的org.apache.catalina.loader.StandardClassLoader类。StandardClassLoader直接继承于java.net中的URLClassLoader类,最终继承于java.lang.ClassLoader类。

而除了这三个loader外,Tomcat中还有个关键的ClassLoader,我也在这里一起介绍,那就是org.apache.catalina.loader.WebappClassLoader类。在实际的Tomcat实例中,会由多个WebappClassLoader类对象,就像其名字说描述的,一个Web app,就有一个这样的loader。

此节上面的第一段中提到,JVM中已经给我们提供了3层类加载器,并通过java.lang.ClassLoader的loadClass()方法实现逻辑,我们可以知道这3层类加载器中使用了“双亲委派”的方式来加载和定义类,从IBM那篇文章中我们也可以看到这一点。这样做的方式,一来大大提高了系统的灵活性和可扩展性,拓宽了Java类定义的空间,二来按照“双亲委派”模式,java.lang包中的系统类只能由系统自身加载,提高了系统的安全性。

对于一般的Java应用开发来讲,我们其实并不需要太过关注JVM所提供的运行环境,不必关注ClassLoader。但在Tomcat中,为了提高系统的灵活性,引入了commonLoader、sharedLoader、catalinaLoader;为了支持和分隔多个web应用,使用了WebappClassLoader。

  • Tomcat中的系统类加载器。Tomcat也是一个Java应用,他也是在最初系统提供的几层类加载器环境下运行起来的。那么Tomcat的一些最基本的类,也和其它简单Java应用一样,是通过系统的类加载器来加载的,比如默认配置下的tomcat/bin目录下的bootstrap.jar、tomcat-juli.jar、commons-daemon.jar这几个jar包中的类。
  • Tomcat的Common Loader。Common Loader是Tomcat在系统类加载器之上建立起来的,其父loader是系统类加载器。Common Loader负责上面几个jar包外的Tomcat的大部分java类,通常情况下是tomcat/lib下的所有jar包。
  • Webapp Class Loader。这个类加载器可以说是Tomcat种最重要的Class Loader,它创造了各个Web app空间。在实现上,它打破了系统默认规则,或者说是打破了java.lang.ClassLoader逻辑中的“双亲委派”模式,提供了一套自定义的类加载流程。默认情况下,对于一个未加载过的类,WebappClassLoader会先让系统加载java.lang.Object等Java本身的基础类,如果不是基础类则优先在当前Web app范围内查找并加载,如果没加载到,再交给common loader走标准的双亲委派模式加载。

对于上面这三点做一些说明:

  • 在tomcat/conf目录的catalina.properties中有common.loader、server.loader、shared.loader的配置,这分别对应着commonLoader、catalinaLoader和sharedLoader。我们可以看到,默认情况下,serverl.loader和shared.loader的配置是空的,这意味着此两者在运行时和commonLoader相同。实际上,在Tomcat5.5之后的版本中,做了简化,只有commonLoader具备实际意义。而在5.5及之前的版本中,三者各不相同,各有分工。可参看官方文档作对比:Tomcat-5.5-ClassLoaderTomcat-7.0-ClassLoader
  • WebappClassLoader的代理关系或者说类查找加载顺序,其实是可以通过delegate来进行配置的。如果配置不同,代码在查找类时会走不同的路劲。

以上,Tomcat7.0的类加载器结构图总体来看大致如下:

      Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ...

有关Tomcat的启动过程和类加载器,本文先总结至此,后面的文章将继续对Tomcat的主体部分基于源代码作分析。

发表在 Java, Java的应用, 开发, 计算机技术 | 标签为 , , , | 7 条评论
第 3 页,共 23 页12345...91215...最旧 »