Kryo簡介及代碼閱讀筆記
更新:2012-08-01
版本 2.16長時間運行可能會導致OOM,版本2.18有bug,不能正確序列化map和collection。
真是悲劇,所用的每一個版本都有bug。不過從代碼來看,作者有時的確比較隨便。。測試用例也少。。(比起msgpack少多了)
========================================
Kryo官方網站:https://code.google.com/p/kryo/
優點:
速度快!見https://github.com/eishay/jvm-serializers/wiki/Staging-Results
支持互相引用,比如類A引用類B,類B引用類A,可以正確地反序列化。
支持多個引用同一個對象,比如多個類引用了同一個對象O,隻會保存一份O的數據。
支持一些有用的注解,如@Tag,@Optional。
支持忽略指定的字段。
支持null
代碼入侵少
代碼比較簡法(比起msgpack,少得多)
缺點:
bug多 2.12,2.14都有bug
文檔比較少,有些使用方法要看代碼才能理解,最新版2.14有bug,不能正確反序列化map類型。
不是跨語言的解決方案
貌似每一個類都要注冊下,不然隻能用writeClassAndObject和readClassAndObject函數。
類要有一個空的構造函數,不然不能序列化。
(如果這個構造函數裏調用了別的資源,而這個資源沒有初始化,那麼就悲劇了。)
可以通過實現KryoSerializable接口來避免這個問題。。同樣不能解決這個問題
Java自帶的則不用調用這個構造函數。
msgpack同樣有這個問題。
接口:
KryoSerializable
KryoCopyable
實現忽略指定的字段
-
使用transient關鍵字
-
使用Context結合@Optional注解
-
使用@Tag注解(很麻煩)
-
實現KryoSerializable接口(比較麻煩,相當於手寫代碼)
引用:
可以用setReferences(boolean )函數來設置,默認是打開的。
引用的實現原理:
原本以為要實現引用是個比較麻煩的事,因為一想到引用,頭腦中就出現一個圖。。但在看了代碼後,發現是比較簡單的。
在Kryo類中有一個writtenObjects的ArrayList,記錄已寫入的對象。有一個readObjects來記錄已寫入的對象。
另外有個depth來記錄深度,每寫一個對象時depth++,當寫完時depth--,當depth == 0時,調用reset函數,清空writtenObjects和。
比如寫一個大對象,這個對象有很多成員,每一個成員都是一個對象,而成員之間有可能用引用關係(A引用了B,B也引用了A)。
private int depth, maxDepth = Integer.MAX_VALUE, nextRegisterID; private final ArrayList writtenObjects = new ArrayList(); private final ArrayList readObjects = new ArrayList();
每當寫一個對象時,都到裏麵去檢查有沒有這個對象,如果有的話,就隻寫一個int即可。這個int是要表明這個對象當前在的位置即可。
因為當反序列化時,可以據讀到的int,正確地從readObjects取回對象。
如果沒有,則在輸出流中寫入writtenObjects的size()+1,再把這個對象放到writtenObjects中。
序列化時,寫入引用的對象在writtenObjects中的位置:
for (int i = 0, n = writtenObjects.size(); i < n; i++) { if (writtenObjects.get(i) == object) { if (DEBUG) debug("kryo", "Write object reference " + i + ": " + string(object)); output.writeInt(i + 1, true); // + 1 because 0 means null. return true; } } // Only write the object the first time encountered in object graph. output.writeInt(writtenObjects.size() + 1, true); writtenObjects.add(object);
反序列化時,據id從readObjects得到正確的對象:
if (--id < readObjects.size()) { Object object = readObjects.get(id); if (DEBUG) debug("kryo", "Read object reference " + id + ": " + string(object)); return object; }
注冊:
貌似每一個類都要注冊下,不然隻能用writeClassAndObject和readClassAndObject函數。
注冊的順序不能亂!!因為是每一個類都有一個id,而這個id是增長的!
可以設置registrationRequired,防止沒有注冊的情況!
注解annotation:
貌似隻有四個:@Optional,@Tag,@DefaultSerializer,@NotNull
實現原理:
每注冊一個類,都有一個id,由一個IntMap的hashMap來保存(TODO,研究下這個東東的實現)
代碼閱讀筆記:
在Kryo類中有以下的成員,簡單來看,就是一些HashMap,用來存放id和Class,Class和id,id和Registration,Class和Registration之間的對應關係:
private final IntMap<Registration> idToRegistration = new IntMap(); private final ObjectMap<Class, Registration> classToRegistration = new ObjectMap(); private final IdentityObjectIntMap<Class> classToNameId = new IdentityObjectIntMap(); private final IntMap<Class> nameIdToClass = new IntMap(); private int nextNameId; //不斷增長,分新的Class分配一個新的id,即Registration中的id
每一個類對應一個Registration:
public class Registration { private final Class type; private final int id; //這個要注意,每一個類都有一個唯一的id,這個id是從0開始不斷增長的 private Serializer serializer; private ObjectInstantiator instantiator; //複製對象時候用 }
直接看這個類的成員,就大概能明白它是怎樣回事了。
要注意一點,Registration中的id很重要,可以說是和別的序列化方案相比,高效之處。
在調用Kryo.writeClass(Output output, Class type)函數時,
先查找到這個類的Registration,得到Serializer,再調用write (Kryo kryo, Output output, T object)寫到輸出流中。
如果沒有找到的話,則為這個類生成一個Registration,並放到Kryo類中的對應的HashMap中。
再來說下Serializer:
默認是FieldSerializer,在生成Registration中,如果為這個類找不到Serializer(到defaultSerializers中找),
則會構造一個FieldSerializer。
FieldSerializer實際是有一個數組存放了每一個field的信息,當調用write (Kryo kryo, Output output, T object)函數時,則曆遍所有的field,把每一個field寫到輸出流中。
private CachedField[] fields = new CachedField[0]; public class CachedField<X> { final Field field; Class fieldClass; Serializer serializer; boolean canBeNull; int accessIndex = -1; }
Kryo有兩種模式,一種是先注冊(regist),再寫對象,即writeObject函數,實際上如果不先注冊,在寫對象時也會注冊,並為class分配一個id。
注意,如果是rpc,則必須兩端都按同樣的順序注冊,否則會出錯,因為必須要明確類對應的唯一id。
另一種是寫類名及對象,即writeClassAndObject函數。
writeClassAndObject函數是先寫入(-1
+ 2)(一個約定的數字),再寫入類ID(第一次要先寫-1,再寫類ID + 類名),寫入引用關係(見引用的實現),最後才寫真正的數據)。
注意每一次writeClassAndObject調用後信息都會清空,所以不用擔心和client交互時會出錯。
代碼中其它有意思的地方:
在writeString函數中先判斷是不是ascii即,所有都<127,如果是在寫string的最後一個字符,會執行這個:
buffer[position - 1] |= 0x80;
不知為何。。
在readString的時候也會判斷:
if ((b & 0x80) == 0) { // ascii
是因為這個https://topic.csdn.net/t/20040416/10/2972001.html ?所謂的雙字節的?
為什麼比其它的序列化方案要快?
為每一個類分配一個id
實現了自己的IntMap
代碼中一些取巧的地方:
利用變量memoizedRegistration和memoizedType記錄上一次的調用writeObject函數的Class,則如果兩次寫入同一類型時,可以直接拿到,不再查找HashMap。
這個也許是為什麼在測試中kryo要比其它類庫要快的原因之一。
注意事項:
實現KryoSerializable接口
像下麵這樣實現是錯誤的。
@Override public void write(Kryo kryo, Output output) { // TODO Auto-generated method stub kryo.writeObject(output, this); } @Override public void read(Kryo kryo, Input input) { // TODO Auto-generated method stub kryo.readObject(input, this.getClass()); }
實際上隻寫入了默認構造函數的內容!
原因是在生成Registration時已在writtenObjects中寫入了這個類,所以在Kryo類中的writtenObjects中已有這個類,所以在調用write函數時,如果是用下麵的代碼,則會以為這個類是已寫過的,所以直接寫了一個1和它的id!!
實際上如果實現了KryoSerializable接口,最終是這個類來調用接口的write函數:KryoSerializableSerializer
正確的寫法是寫入每一個成員,在read函數中把數據讀出,再賦值給成員。
最後更新:2017-04-02 16:47:52