對象是我們可以進行研究的任何事物,世間萬物都可以看作對象。它不僅可以表示我們可以看到的具體事物,也可以表示那些我們看不見的事件等。對象是一個實體,它具有狀態,一般我們用變量來表示,同時它也可以具有操作行為,一般用方法來表示,對象就是對象狀態和對象行為的集合體。
在之前我們很多次的說到類,對于對象來說,具有相同或相似性質的對象的抽象就是類。因此,對象的抽象是類,類的具體化就是對象,我們常常也說對象是類的實例。從對象的表現形式來看,它和一般的數據類型在形式上十分相似,但是它們在本質是不同的。對象擁有方法,對象間的通信是通過方法調用,以一種消息傳遞的方式進行。而我們常說的面向對象編程(OOP)使得對象具有交互能力的主要模型就是消息傳遞模型。對象是消息傳遞的主體,它可以接收,也可以拒絕外界發來的消息。
這一小節,我們從源碼結構來看看PHP實現對象的方法以及其消息傳遞的方式。
## 對象的結構[]()
在第三章[<< 第一節 變量的內部結構 >>](#)中提到:對象在PHP中是使用一種zend_object_value的結構體來存儲。
typedef struct _zend_object_value {
zend_object_handle handle;
// unsigned int類型,EG(objects_store).object_buckets的索引
zend_object_handlers *handlers;
} zend_object_value;
PHP內核會將所有的對象存放在一個對象列表容器中,這個列表容器是保存在EG(objects_store)里的一個全局變量。上面的handle字段就是這個列表中object_buckets的索引。當我們需要在PHP中存儲對象的時候,PHP內核會根據handle索引從對象列表中獲取相對應的對象。而獲取的對象有其獨立的結構,如下代碼所示:
typedef struct _zend_object {
zend_class_entry *ce;
HashTable *properties;
HashTable *guards; /* protects from __get/__set ... recursion */
} zend_object;
ce是存儲該對象的類結構,properties是一個HashTable,用來存放對象的屬性。
在zend_object_value結構體中除了索引字段外還有一個包含對象處理方法的字段:handlers。它的類型是zend_object_handlers,我們可以在Zend/zend_object_handlers.h文件中找到它的定義。這是一個包含了多個指針函數的結構體,這些指針函數包括對對象屬性的操作,對對象方法的操作,克隆等。此字段會在對象創建的時候初始化。
## 對象的創建[]()
在PHP代碼中,對象的創建是通過關鍵字 **new** 進行的。一個new操作最終會產生三個opcode,這三個opcode實際為創建對象的三個步驟:
1. ZEND_FETCH_CLASS 根據類名獲取存儲類的變量,其實現為一個hashtalbe EG(class_table) 的查找操作;
1. NEW 初始化對象,并將EX(call)->fbc指向構造函數指針,初始化的操作我們在后面詳細說明,其最后執行的函數為 ZEND_NEW_SPEC_HANDLER
1. DO_FCALL_BY_NAME 調用構造函數,其調用和其它的函數調用是一樣,都是調用zend_do_fcall_common_helper_SPEC
第一步和第三步比較簡單,我們詳細介紹一下第二步:初始化對象。初始化對象調用ZEND_NEW_SPEC_HANDLER,它首先會判斷對象所對應的類是否為可實例化的類,即判斷類的ce_flags是否與ZEND_ACC_INTERFACE、ZEND_ACC_IMPLICIT_ABSTRACT_CLASS或ZEND_ACC_EXPLICIT_ABSTRACT_CLASS有交集,即判斷類是否為接口或抽象類。
> 此處的抽象類包括直接聲明的抽象類或因為包含了抽象方法而被聲明的抽象類
在類的類型判斷完成后,如果一切正常,程序會給需要創建的對象存放的ZVAL容器分配內存。然后調用object_init_ex方法初始化類,其調用順序為:[object_init_ex()] --> [_object_init_ex()] --> [_object_and_properties_init()]
在_object_and_properties_init函數中,程序會執行前面提到的類的類型的判斷,然后更新類的靜態變量等信息(在這前面的章節有說明),更新完成后,程序會設置zval的類型為IS_OBJECT。
Z_TYPE_P(arg) = IS_OBJECT;
在設置了類型之后,程序會執行zend_object類型的對象的初始化工作,此時調用的函數是zend_objects_new。
ZEND_API zend_object_value zend_objects_new(zend_object **object, zend_class_entry *class_type TSRMLS_DC)
{
zend_object_value retval;
?
*object = emalloc(sizeof(zend_object));
(*object)->ce = class_type;
retval.handle = zend_objects_store_put(*object, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) zend_objects_free_object_storage, NULL TSRMLS_CC);
retval.handlers = &std_object_handlers;
(*object)->guards = NULL;
return retval;
}
zend_objects_new函數會初始化對象自身的相關信息,包括對象歸屬于的類,對象實體的存儲索引,對象的相關處理函數。在這里將對象放入對象池中的函數為zend_objects_store_put。
在將對象放入對象池,返回對象的存放索引后,程序設置對象的處理函數為標準對象處理函數:std_object_handlers。其位于Zend/zend_object_handles.c文件中。
### 對象池[]()
這里針對對象,我們引入一個新的概念--對象池。我們將PHP內核在運行中存儲所有對象的列表稱之為對象池,即EG(objects_store)。這個對象池的作用是存儲PHP中間代碼運行階段所有生成的對象,這個思想有點類似于我們做數據庫表設計時,當一個實例與另一個實體存在一對多的關系時,將多的那一端對應的實體提取出來存儲在一個獨立的表一樣。這樣做的好處有兩個,一個是可以對象復用,另一個是節省內存,特別是在對象很大,并且我們不需要用到對象的所有信息時。對象池的存儲結構為zend_objects_store結構體,如下;
typedef struct _zend_objects_store {
zend_object_store_bucket *object_buckets;
zend_uint top;
zend_uint size;
int free_list_head;
} zend_objects_store;
?
typedef struct _zend_object_store_bucket {
zend_bool destructor_called;
zend_bool valid;
union _store_bucket {
struct _store_object {
void *object;
zend_objects_store_dtor_t dtor;
zend_objects_free_object_storage_t free_storage;
zend_objects_store_clone_t clone;
const zend_object_handlers *handlers;
zend_uint refcount;
gc_root_buffer *buffered;
} obj;
struct {
int next;
} free_list;
} bucket;
} zend_object_store_bucket;
針對對象池,PHP內核有一套對象操作API,位于Zend/zend_objects_API.c文件,其列表如下:
- zend_objects_store_init 對象池初始化操作,它的執行階段是請求初始化階段,執行順序是:[php_request_startup] --> [php_start_sapi] --> [zend_activate] --> [init_executor]初始化時,它會分配1024個zend_object_store_bucket給對象池。
- zend_objects_store_destroy 銷毀對象池,調用efree釋放內存
- zend_objects_store_mark_destructed 標記所有對象已經調用了析構函數
- zend_objects_store_free_object_storage 釋放存儲的對象
- zend_objects_store_put 對象的添加API,在此函數中,程序會執行單個bucket的初始化操作
- zend_objects_store_get_refcount 獲取對象池中對象的引用計數
- zend_objects_store_add_ref 對象的引用計數加1,傳入值為對象
- zend_objects_store_add_ref_by_handle 通過handle查找對象,并將其引用計數加1
- zend_objects_store_del_ref 對象的引用計數減1,傳入值為對象
- zend_objects_store_del_ref_by_handle_ex 通過handle查找對象,并將其引用計數減1,對于引用計數為1的對象有清除處理
- zend_objects_store_clone_obj 對象克隆API,構造一個新的bucket,并將新的對象添加到對象池
- zend_object_store_get_object 獲取對象池中bucket中的對象,傳入值為對象
- zend_object_store_get_object_by_handle 獲取對象池中bucket中的對象,傳入值為索引值
## 成員變量[]()
從前面的對象結構來看,對象的成員變量存儲在properties參數中。并且每個對象都會有一套標準的操作函數,如果需要獲取成員變量,對象最后調用的是read_property,其對應的標準函數為zend_std_read_property;如果需要設置成員變量,對象最后調用的是write_property,其對應的標準函數zend_std_write_property。這些函數都是可以定制的,如果有不同的需求,可以通過設置對應的函數指針替換。如在dom擴展中,它的變量的獲取函數和設置函數都是定制的。
/* {{{ PHP_MINIT_FUNCTION(dom) */
PHP_MINIT_FUNCTION(dom)
{
zend_class_entry ce;
?
memcpy(&dom_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
dom_object_handlers.read_property = dom_read_property;
dom_object_handlers.write_property = dom_write_property;
// ...省略
}
以上是dom擴展的模塊初始化函數的部分內容,在這里,它替換了對象的read_property方法等。
這里我們以標準的操作函數為例說明成員變量的讀取和獲取。成員變量的獲取最終調用的是zend_std_read_property函數。這個函數的流程是這樣的:
- 第一步,獲取對象的屬性,如果存在,轉第二步;如果沒有相關屬性,轉第三步
- 第二步,從對象的properties查找是否存在與名稱對應的屬性存在,如果存在返回結果,如果不存在,轉第三步
- 第三步,如果存在__get魔術方法,則調用此方法獲取變量,如果不存在,轉第四步
- 第四步,如果type=BP_VAR_IS,返回 &EG(uninitialized_zval_ptr),否則報錯
成員變量的設置最終調用的是zend_std_write_property函數。整個執行流程如下:
- 第一步,獲取對象的屬性,如果存在,轉第二步;如果沒有相關屬性,轉第四步
- 第二步,從對象的properties查找是否存在與名稱對應的屬性存在,如果存在,轉第三步,如果不存在,轉第四步
- 第三步,如果已有的值和需要設置的值相同,則不執行任何操作,否則執行變量賦值操作,此處的變量賦值操作和常規的變量賦值類似,有一些區別,這里只處理了是否引用的問題
- 第四步,如果存在__set魔術方法,則調用此方法設置變量,如果不存在,轉第五步
- 第五步,如果成員變量一直沒有被設置過,則直接將此變量添加到對象的properties字段所在HashTable中。
## 成員方法[]()
成員方法又包括常規的成員方法和魔術方法。魔術方法在前面的第五小節已經介紹過了,這里就不再贅述。在對象的標準函數中并沒有成員方法的調用函數,默認情況下設置為NULL。在SPL擴展中,有此函數的調用設置,如下代碼:
PHP_MINIT_FUNCTION(spl_iterators)
{
// ...省略
memcpy(&spl_handlers_dual_it, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
spl_handlers_dual_it.get_method = spl_dual_it_get_method;
/*spl_handlers_dual_it.call_method = spl_dual_it_call_method;*/
spl_handlers_dual_it.clone_obj = NULL;
?
// ...省略
}
以下面的PHP代碼為例,我們看看成員方法的調用過程:
class Tipi {
public function t() {
[echo](http://www.php.net/echo) 'tipi';
}
}
?
$obj = new Tipi();
$obj->t();
這是一個簡單的類實現,它僅有一個成員方法叫t。創建一個此類的實例,將其賦值給變量$obj,通過這個對象變量執行其成員方法。使用VLD擴展查看其生成的中間代碼,可以知道其過程分為初始化成員方法的調用,執行方法兩個過程。初始化成員方法的調用對應的中間代碼為ZEND_INIT_METHOD_CALL,從我們的調用方式(一個為CV,一個為CONST)可知其對應的執行函數為 **ZEND_INIT_METHOD_CALL_SPEC_CV_CONST_HANDLER**此函數的調用流程如下:
- 第一步,處理調用的方法名,獲取其值,并做檢驗處理:如果不是字符串,則報錯
- 第二步,如果第一個操作數是對象,則轉第三步,否則報錯 Call to a member function t on a non-object
- 第三步,調用對象的get_method函數獲取成員方法
- 第四步,其它處理,包括靜態方法,this變量等。
而get_method函數一般是指標準實現中的get_method函數,其對應的具體函數為Zend/zend_object_handlers.c文件中zend_std_get_method函數。zend_std_get_method函數的流程如下:
- 第一步,從zobj->ce->function_table中查找是否存在需要調用的函數,如果不存在,轉第二步,如果存在,轉第三步
- 第二步,如果__call函數存在,則調用zend_get_user_call_function函數獲取并返回,如果不存在,則返回NULL
- 第三步,檢查方法的訪問控制,如果為私有函數,轉第四步,否則轉第五步
- 第四步,如果為同一個類或父類和這個方法在同一個作用域范圍,則返回此方法,否則判斷__call函數是否存在,存在則調用此函數,否則報錯
- 第五步,處理函數重載及訪問控制為protected的情況。 轉第六步
- 第六步,返回fbc
在獲得了函數的信息后,下面的操作就是執行了,關于函數的執行在第四章已經介紹過了。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊