您好,登錄后才能下訂單哦!
寫在前面
序列化是一個轉儲-恢復的操作過程,即支持將一個對象轉儲到臨時緩沖或者永久文件中和恢復臨時緩沖或者永久文件中的內容到一個對象中等操作,其目的是可以在不同的應用程序之間共享和傳輸數(shù)據(jù),以達到跨應用程序、跨語言和跨平臺的解耦,以及當應用程序在客戶現(xiàn)場發(fā)生異?;蛘弑罎r可以即時保存數(shù)據(jù)結構各內容的值到文件中,并在發(fā)回給開發(fā)者時再恢復數(shù)據(jù)結構各內容的值以協(xié)助分析和定位原因。
泛型編程是一個對具有相同功能的不同類型的抽象實現(xiàn)過程,比如STL的源碼實現(xiàn),其支持在編譯期由編譯器自動推導具體類型并生成實現(xiàn)代碼,同時依據(jù)具體類型的特定性質或者優(yōu)化需要支持使用特化或者偏特化及模板元編程等特性進行具體實現(xiàn)。
Hello World
#include <iostream>
int main(int argc, char* argv[])
{
std::cout << "Hello World!" << std::endl;
return 0;
}
泛型編程其實就在我們身邊,我們經(jīng)常使用的std和stl命名空間中的函數(shù)和類很多都是泛型編程實現(xiàn)的,如上述代碼中的std::cout即是模板類std::basic_ostream的一種特化
namespace std
{
typedef basic_ostream<char> ostream;
}
從C++的標準輸入輸出開始
除了上述提到的std::cout和std::basic_ostream外,C++還提供了各種形式的輸入輸出模板類,如std::basic_istream,std::basic_ifstream,std::basic_ofstream, std::basic_istringstream,std::basic_ostringstream等等,其主要實現(xiàn)了內建類型(built-in)的輸入輸出接口,比如對于Hello World可直接使用于字符串,然而對于自定義類型的輸入輸出,則需要重載實現(xiàn)操作符>>和<<,如對于下面的自定義類
class MyClip
{
bool mValid;
int mIn;
int mOut;
std::string mFilePath;
};
如使用下面的方式則會出現(xiàn)一連串的編譯錯誤
MyClip clip;
std::cout << clip;
錯誤內容大致都是一些clip不支持<<操作符并在嘗試將clip轉為cout支持的一系列的內建類型如void*和int等等類型時轉換操作不支持等信息。
為了解決編譯錯誤,我們則需要將類MyClip支持輸入輸出操作符>>和<<,類似實現(xiàn)代碼如下
inline std::istream& operator>>(std::istream& st, MyClip& clip)
{
st >> clip.mValid;
st >> clip.mIn >> clip.mOut;
st >> clip.mFilePath;
return st;
}
inline std::ostream& operator<<(std::ostream& st, MyClip const& clip)
{
st << clip.mValid << ' ';
st << clip.mIn << ' ' << clip.mOut << ' ';
st << clip.mFilePath << ' ';
return st;
}
為了能正常訪問類對象的私有成員變量,我們還需要在自定義類型里面增加序列化和反序列化的友元函數(shù)(回憶一下這里為何必須使用友元函數(shù)而不能直接重載操作符>>和<<?),如
friend std::istream& operator>>(std::istream& st, MyClip& clip);
friend std::ostream& operator<<(std::ostream& st, MyClip const& clip);
這種序列化的實現(xiàn)方法是非常直觀而且容易理解的,但缺陷是對于大型的項目開發(fā)中,由于自定義類型的數(shù)量較多,可能達到成千上萬個甚至更多時,對于每個類型我們則需要實現(xiàn)2個函數(shù),一個是序列化轉儲數(shù)據(jù),另一個則是反序列化恢復數(shù)據(jù),不僅僅增加了開發(fā)實現(xiàn)的代碼數(shù)量,如果后期一旦對部分類的成員變量有所修改,則需要同時修改這2個函數(shù)。
同時考慮到更復雜的自定義類型,比如含有繼承關系和自定義類型的成員變量
class MyVideo : public MyClip
{
std::list<MyFilter> mFilters;
};
上述代碼需要轉儲-恢復類MyVideo的對象內容時,事情會變得更復雜些,因為還需要轉儲-恢復基類,同時成員變量使用了STL模板容器list與自定義類'MyFilter`的結合,這種情況也需要自己去定義轉儲-恢復的實現(xiàn)方式。
針對以上疑問,有沒有一種方法能減少我們代碼修改的工作量,同時又易于理解和維護呢?
Boost序列化庫
對于使用C++標準輸入輸出的方法遇到的問題,好在Boost提供了一種良好的解決方式,則是將所有類型的轉儲-恢復操作抽象到一個函數(shù)中,易于理解,如對于上述類型,只需要將上述的2個友元函數(shù)替換為下面的一個友元函數(shù)
template<typename Archive> friend void serialize(Archive&, MyClip&, unsigned int const);
友元函數(shù)的實現(xiàn)類似下面的樣子
template<typename A>void serialize(A &ar, MyClip &clip, unsigned int const ver)
{
ar & BOOST_SERIALIZATION_NVP(clip.mValid);
ar & BOOST_SERIALIZATION_NVP(clip.mIn);
ar & BOOST_SERIALIZATION_NVP(clip.mOut);
ar & BOOST_SERIALIZATION_NVP(clip.mFilePath);
}
其中BOOST_SERIALIZATION_NVP是Boost內部定義的一個宏,其主要作用是對各個變量進行打包。
轉儲-恢復的使用則直接作用于操作符>>和<<,比如
// store
MyClip clip;
······
std::ostringstream ostr;
boost::archive::text_oarchive oa(ostr);
oa << clip;
// load
std::istringstream istr(ostr.str());
boost::archive::text_iarchive ia(istr);
ia >> clip;
這里使用的std::istringstream和std::ostringstream即是分別從字符串流中恢復數(shù)據(jù)以及將類對象的數(shù)據(jù)轉儲到字符串流中。
對于類MyFilter和MyVideo則使用相同的方式,即分別增加一個友元模板函數(shù)serialize的實現(xiàn)即可,至于std::list模板類,boost已經(jīng)幫我們實現(xiàn)了。
這時我們發(fā)現(xiàn),對于每一個定義的類,我們需要做的僅僅是在類內部聲明一個友元模板函數(shù),同時類外部實現(xiàn)這個模板函數(shù)即可,對于后期類的成員變量的修改,如增加、刪除或者重命名成員變量,也僅僅是修改一個函數(shù)即可。
Boost序列化庫已經(jīng)足夠完美了,但故事并未結束!
在用于端上開發(fā)時,我們發(fā)現(xiàn)引用Boost序列化庫遇到了幾個挑戰(zhàn)
端上的編譯資料很少,官方對端上編譯的資料基本沒有,在切換不同的版本進行編譯時經(jīng)常會遇到各種奇怪的編譯錯誤問題
Boost在不同的C++開發(fā)標準之間兼容性不夠好,尤其是使用libc++標準進行編譯鏈接時遇到的問題較多
Boost增加了端上發(fā)行包的體積
Boost每次序列化都會增加序列化庫及版本號等私有頭信息,反序列化時再重新解析,降低了部分場景下的使用性能
基于泛型編程的序列化實現(xiàn)方法
為了解決使用Boost遇到的這些問題,我們覺得有必要重新實現(xiàn)序列化庫,以剝離對Boost的依賴,同時能滿足如下要求
由于現(xiàn)有工程大量使用了Boost序列化庫,因此兼容現(xiàn)有的代碼以及開發(fā)者的習慣是首要目標
盡量使得代碼修改和重構的工作量最小
兼容不同的C++開發(fā)標準
提供比Boost序列化庫更高的性能
降低端上發(fā)行包的體積
為了兼容現(xiàn)有使用Boost的代碼以及保持當前開發(fā)者的習慣,同時使用代碼修改的重構的工作量最小,我們應該保留模板函數(shù)serialize,同時對于模板函數(shù)內部的實現(xiàn),為了提高效率也不需要對各成員變量重新打包,即直接使用如下定義
#define BOOST_SERIALIZATION_NVP(value) value
對于轉儲-恢復的接口調用,仍然延續(xù)目前的調用方式,只是將輸入輸出類修改為
alivc::text_oarchive oa(ostr);
alivc::text_iarchive ia(istr);
好了,到此為止,序列化庫對外的接口工作已經(jīng)做好,剩下的就是內部的事情,應該如何重新設計和實現(xiàn)序列化庫的內部框架才能滿足要求呢?
先來看一下當前的設計架構的處理流程圖
比如對于轉儲類text_oarchive,其支持的接口必須包括
explicit text_oarchive(std::ostream& ost, unsigned int version = 0);
template <typename T> text_oarchive& operator<<(T& v);
template <typename T> text_oarchive& operator&(T& v);
開發(fā)者調用操作符函數(shù)<<時,需要首先回調到相應類型的模板函數(shù)serialize中
template <typename T>
text_oarchive& operator<<(T& v)
{
serialize(*this, v, mversion);
return *this;
}
當開始對具體類型的各個成員進行操作時,這時需要進行判斷,如果此成員變量的類型已經(jīng)是內建類型,則直接進行序列化,如果是自定義類型,則需要重新回調到對應類型的模板函數(shù)serialize中
template <typename T>
text_oarchive& operator&(T& v)
{
basic_save<T>::invoke(*this, v, mversion);
return *this;
}
上述代碼中的basic_save::invoke則會在編譯期完成模板類型推導并選擇直接對內建類型進行轉儲還是重新回調到成員變量對應類型的serialize函數(shù)繼續(xù)重復上述過程。
由于內建類型數(shù)量有限,因此這里我們選擇使模板類basic_save的默認行為為回調到相應類型的serialize函數(shù)中
template <typename T, bool E = false>
struct basic_load_save
{
template <typename A>
static void invoke(A& ar, T& v, unsigned int version)
{
serialize(ar, v, version);
}
};
template <typename T>
struct basic_save : public basic_load_save<T, std::is_enum<T>::value>
{
};
這時會發(fā)現(xiàn)上述代碼的模板參數(shù)多了一個參數(shù)E,這里主要是需要對枚舉類型進行特殊處理,使用偏特化的實現(xiàn)如下
template <typename T>
struct basic_load_save<T, true>
{
template <typename A>
static void invoke(A& ar, T& v, unsigned int version)
{
int tmp = v;
ar & tmp;
v = (T)tmp;
}
};
到這里我們已經(jīng)完成了重載操作符&的默認行為,即是不斷進行回溯到相應的成員變量的類型中的模板函數(shù)serialize中,但對于碰到內建模型時,我們則需要讓這個回溯過程停止,比如對于int類型
template <typename T>
struct basic_pod_save
{
template <typename A>
static void invoke(A& ar, T const& v, unsigned int)
{
ar.template save(v);
}
};
template <>
struct basic_save<int> : public basic_pod_save<int>
{
};
這里對于int類型,則直接轉儲整數(shù)值到輸出流中,此時text_oarchive則還需要增加一個終極轉儲函數(shù)
template <typename T>
void save(T const& v)
{
most << v << ' ';
}
這里我們發(fā)現(xiàn),在save成員函數(shù)中,我們已經(jīng)將具體的成員變量的值輸出到流中了。
對于其它的內建類型,則使用相同的方式處理,要以參考C++ std::basic_ostream的源碼實現(xiàn)。
相應的,對于恢復操作的text_iarchive的操作流程如下圖
測試結果
我們對使用Boost以及重新實現(xiàn)的序列化庫進行了對比測試,其結果如下
代碼修改的重構的工作非常小,只需要刪除Boost的相關頭文件,以及將boost相關命名空間替換為alivc,BOOST_SERIALIZATION_FUNCTION以及BOOST_SERIALIZATION_NVP的宏替換
Android端下的發(fā)行包體積大概減少了500KB
目前的消息處理框架中,處理一次消息的平均時間由100us降低到了25us
代碼實現(xiàn)約300行,更輕量級
未來還能做什么
由于當前項目的原因,重新實現(xiàn)的序列化還沒有支持轉儲-恢復指針所指向的內存數(shù)據(jù),但當前的設計框架已經(jīng)考慮了這種拓展性,未來會考慮支持。
總結
泛型編程能夠大幅提高開發(fā)效率,尤其是在代碼重用方面能發(fā)揮其優(yōu)勢,同時由于其類型推導及生成代碼均在編譯期完成,并不會降低性能
序列化對于需要進行轉儲-恢復的解耦處理以及協(xié)助定位異常和崩潰的原因分析具有重要作用
利用C++及模板自身的語言特性優(yōu)勢,結合合理的架構設計,即易于拓展又能盡量避免過度設計
覺得不錯請點贊支持,歡迎留言或進我的個人群855801563領取【架構資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本群專用于學習交流技術、分享面試機會,拒絕廣告,我也會在群內不定期答題、探討。
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。