序言
本來是在講解List接口系列的集合,但是接下來我要講的是那個HashSet,要明白HashSet就必須先要明白HashMap,是以在此出穿插一篇hashMap的文章,為了更好的學習HashSet。個人感覺初次看HashMap源碼比較難,但是明白了,其實也不是很難,
--WZY
一、準備工作。
建議:先去看一下我的另一篇講解hashcode的文章,讓自己知道為什麼使用hashcode值進行查詢會很快。如果你已經懂了hashcode的工作原理,那麼就可以直接往下看了。
http://www.cnblogs.com/whgk/p/6071617.html1、連結清單散列
什麼是連結清單散列呢?通過數組和連結清單結合在一起使用,就叫做連結清單散列。這其實就是hashmap存儲的原理圖。
2、hashMap的資料結構和存儲原理
HashMap的資料結構就是用的連結清單散列,大概是怎麼存儲的呢?分兩步
1、HashMap内部有一個entry的内部類,其中有四個屬性,我們要存儲一個值,則需要一個key和一個value,存到map中就會先将key和value儲存在這個Entry類建立的對象中。
//這裡隻看這一小部分,其他重點的在下面詳細解釋
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //就是我們說的map的key
V value; //value值,這兩個都不陌生
Entry<K,V> next;//指向下一個entry對象
int hash;//通過key算過來的你hashcode值。
實體模型就是這樣
2、構造好了entry對象,然後将該對象放入數組中,如何存放就是這hashMap的精華所在了。
大概的一個存放過程是:通過entry對象中的hash值來确定将該對象存放在數組中的哪個位置上,如果在這個位置上還有其他元素,則通過連結清單來存儲這個元素。
3、HashMap存放元素的大概過程?
通過key、value封裝成一個entry對象,然後通過key的值來計算該entry的hash值,通過entry的hash值和數組的長度length來計算出entry放在數組中的哪個位置上面,每次存放都是将entry放在第一個位置。在這個過程中,就是通過hash
3、loadFactor加載因子
loadFactor加載因子是控制數組存放資料的疏密程度,loadFactor越趨近于1,那麼數組中存放的資料(entry)也就越多,也就越密,也就是會讓連結清單的長度增加,loadFactor越小,也就是趨近于0,那麼數組中存放的資料也就越稀,也就是可能數組中每個位置上就放一個元素。那有人說,就把loadFactor變為1最好嗎,存的資料很多,但是這樣會有一個問題,就是我們在通過key拿到我們的value時,是先通過key的hashcode值,找到對應數組中的位置,如果該位置中有很多元素,則需要通過equals來依次比較連結清單中的元素,拿到我們的value值,這樣花費的性能就很高,如果能讓數組上的每個位置盡量隻有一個元素最好,我們就能直接得到value值了,是以有人又會說,那把loadFactor變得很小不就好了,但是如果變得太小,在數組中的位置就會太稀,也就是分散的太開,浪費很多空間,這樣也不好,是以在hashMap中loadFactor的初始值就是0.75,一般情況下不需要更改它。
4、Size的意思?
size就是在該HashMap的執行個體中實際存儲的元素的個數
5、threshold的作用?
threshold = capacity * loadFactor,當Size>=threshold的時候,那麼就要考慮對數組的擴增了,也就是說,這個的意思就是衡量數組是否需要擴增的一個标準。注意這裡說的是考慮,因為實際上要擴增數組,除了這個size>=threshold條件外,還需要另外一個條件,這個就等在源碼中看吧。
6、什麼是桶?
根據前面畫的HashMap存儲的資料結構圖,你這樣想,數組中每一個位置上都放有一個桶,每個桶裡就是裝一個連結清單,連結清單中可以有很多個元素(entry),這就是桶的意思。也就相當于把元素都放在桶中。
7、capacity
這個就代表的數組的容量,也就是數組的長度,同時也是HashMap中桶的個數。預設值是16.
通過一張截圖和圖中的文字,來熟悉一下我們上面說的各種屬性,介紹這些屬性的博文:
http://blog.csdn.net/fan2012huan/article/details/51087722
二、初識HashMap
慣例:檢視HashMapAPI,申明一下,如果還暫時看不懂說的是什麼意思,很正常,有疑惑的地方不用一直抓着不放,先看下面的源碼分析,然後再回過頭來看這個api文檔講的東西。就會發現pai說的東西就是我們源碼中看到的那樣。
//1、哈希表基于map接口的實作,這個實作提供了map所有的操作,并且提供了key和value可以為null,(HashMap和HashTable大緻上市一樣的除了hashmap是異步的和允許key和value為null),
這個類不确定map中元素的位置,特别要提的是,這個類也不确定元素的位置随着時間會不會保持不變。
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key.
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
in particular, it does not guarantee that the order will remain constant over time.
//假設哈希函數将元素合适的分到了每個桶(其實就是指的數組中位置上的連結清單)中,則這個實作為基本的操作(get、put)提供了穩定的性能,疊代這個集合視圖需要的時間跟hashMap執行個體(key-value映射的數量)的容量(在桶中)
成正比,是以,如果疊代的性能很重要的話,就不要将初始容量設定的太高或者loadfactor設定的太低,【這裡的桶,相當于在數組中每個位置上放一個桶裝元素】
This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.
Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings
). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
//HashMap的執行個體有兩個參數影響性能,初始化容量(initialCapacity)和loadFactor加載因子,在哈希表中這個容量是桶的數量【也就是數組的長度】,一個初始化容量僅僅是在哈希表被建立時容量,在
容量自動增長之前加載因子是衡量哈希表被允許達到的多少的。當entry的數量在哈希表中超過了加載因子乘以目前的容量,那麼哈希表被修改(内部的資料結構會被重建立立)是以哈希表有大約兩倍的桶的數量
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table,
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
//通常來講,預設的加載因子(0.75)能夠在時間和空間上提供一個好的平衡,更高的值會減少空間上的開支但是會增加查詢花費的時間(展現在HashMap類中get、put方法上),當設定初始化容量時,應該考慮到map中會存放
entry的數量和加載因子,以便最少次數的進行rehash操作,如果初始容量大于最大條目數除以加載因子,則不會發生 rehash 操作。
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup
cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken
into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of
entries divided by the load factor, no rehash operations will ever occur.
//如果很多映射關系要存儲在 HashMap 執行個體中,則相對于按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量建立它将使得映射關系能更有效地存儲。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting
it perform automatic rehashing as needed to grow the table
三、HashMap的繼承結構和實作的接口
繼承結構很簡單:上面就繼承了一個abstractMap,也就是用來減輕實作Map接口的編寫負擔
實作的接口:
Map<K,V>:在AbstractMap抽象類中已經實作過的接口,這裡又實作,實際上是多餘的。但每個集合都有這樣的錯誤,也沒過大影響
Cloneable:能夠使用Clone()方法
Serializable:能夠使之序列化
四、HashMap的構造方法
有四個構造方法,構造方法的作用就是記錄一下16這個數給threshold(這個數值最終會當作第一次數組的長度。),和初始化加載因子。注意,hashMap中table數組一開始就已經是個沒有長度的數組了。
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
HashMap()
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
//看上面的注釋就已經知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75
//初始化容量:也就是初始化數組的大小
//加載因子:數組上的存放資料疏密程度。
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
HashMap(int)
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(int,float)
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)//initialCapacity不能小于0
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//initialCapacity大于最大容量時
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//isNaN的作用是檢測loadFactor是否是一個浮點數
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor; //初始化loadFactor
threshold = initialCapacity;//将threshold變為16,其實這裡指派為16沒什麼用,隻是單純的記錄一下這個16的值,好在後面講此值給數組的長度。後面再初始化數組長度的時候,就會把該值給重新指派
init();//一個空的初始化方法
}
HashMap(Map<? extends K, ? extends V> m)
//将參數Map轉換為一個HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
//根據m中的size來設定初始化容量為多少合适,如果(m.size()/DEFAULT_LOAD_FACTOR)+1>DEFAULT_INITLAL_CAPACITY那麼久取(m.size()/DEFAULT_LOAD_FACTOR)+1為初始化容量,反之取預設的,而加載因子就一直是預設的0.75
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//增大table的容量的方法,table也就是連結清單散列的數組。threshold就是初始化的大小(m.size()/DEFAULT_LOAD_FACTOR)+1或者DEFAULT_INITLAL_CAPACITY16
inflateTable(threshold);
putAllForCreate(m);
}
inflateTable(threshold):這個方法隻是在第一次對數組進行操作的時候,需要對數組進行增加來存儲元素,因為其中什麼元素都沒有,就調用該方法。
/**
* Inflates the table.
*/
//擴充table的功能
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//傳回一個大于等于 最接近toSize的2的幂數。 什麼意思呢?2的3次方的幂數就是8.2的幂數就是每次都市2的幾次方,2的幂數有可能是1,2,4,8,16等。
比如toSize=16,則16的2的幂數就是2的4次方還是16,比如toSize=17,那麼最接近他的2的幂數就是2的5次方,也就是32.
//這裡不點進去看了,因為裡面就是為了實作上面這個目的而寫的一些算法,有興趣的可以自己去讀一讀
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//設定一個需要擴增數組的一個标準 上面也有說到這個變量,不清楚的回頭再看看
//将table數組大大小改變為capacity。
table = new Entry[capacity];
//初始化一個容量為capacity的哈希表,等用到的時候才真正初始化,傳回值是boolean,先放一放。。。。。。。。。。
initHashSeedAsNeeded(capacity);
}
五、常用的方法
介紹方法前,先看一下entry這個内部類,前面剛開始介紹了那麼多,現在來看一下entry類中是如何建構一個什麼樣的結構
、put(K,V) 這一個put方法,真是有一大堆,大家應該慢慢消化完。一步一步,不懂得就看我寫的注釋,看看到底做了些什麼。
//添加元素
public V put(K key, V value) {
if (table == EMPTY_TABLE) {//判斷是不是一個空的數組,也就是數組沒有長度,通過構造方法建立,還沒開始用,是以長度為0,這裡就開始對數組進行增長。
inflateTable(threshold);//這個方法在第四個構造方法中介紹過,就是用來将數組變為大于等于threshold的2次幂。一開始threshold為16,那麼根據算法,數組的開始長度也就是為16.
}
if (key == null)//這裡可以看到,HashMap為什麼可以使用null值了。
return putForNullKey(value);//将key為null的值存放到table中去。具體看下面putForNullKey的分析。
int hash = hash(key);//通過hash函數來将key轉換為一個hash值
int i = indexFor(hash, table.length);//通過這個方法,将key轉換來的hash值和數組的長度進行操作,得到在數組中的位置。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//在對應位置上加入新元素
Object k;
//周遊這個桶中的元素,看有沒有相同的key值。
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//隻有相同的hash值并且 (可能key相同但是Key的hashcode不同也算key一樣或者用equals比較得到相同)這說明裡面有相同的key值。
V oldValue = e.value;//将老value用新value來替代。
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);//增加元素,方法内部實作很簡單,就是将新增加的元素放第一位。而不是往後追加。
return null;
}
putForNullKey
//key為null的元素,預設放在table數組中的第一個位置上。并且可以知道,如果第一個位置上有元素,則将原來的值覆寫掉,如果第一個位置上沒有entry。那麼就将自己放在第一個位置。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {//周遊在數組第一個位置上的連結清單的每個元素(entry),其實就一個,因為null就一個。
if (e.key == null) {//如果發現有key為null的值,将現在的值指派給原來key為null的value。
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//一個空的方法。
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//上面的情況是有key為null的元素。現在這裡是沒有key為null的元素,則要在第一個位置上放上自己。請看下面對這個方法的解析。
return null;
}
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//增加元素前,看一下元素的個數是否大于等于了我們規定存放在數組中的個數(threshold=數組容量*加載因子,隻是一個存放判定數組是否需要擴增标準的變量),并且在table這個指定位置上有元素,則對數組進行擴充
//前面一個條件成立,擴充數組,可以了解,為什麼還要加上後面一個條件呢?原因是:我們希望盡量在每個數組的每個位置上隻有一個元素是最好的,數組的容量是大于threshold的,也就是說size雖然到了要擴增的那個标準,
但是在數組中可能還是有很多位置上沒有放元素,是以在這些位置上增加元素是合理的,不需要擴增。隻有等到在size達到了擴增的标準并且添加元素的位置上有别的元素的情況下,才進行闊增。
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//擴增數組,看下面的分析。
hash = (null != key) ? hash(key) : 0;//擴增完數組後,原來的那些參數就沒用了。需要重新計算,計算添加元素的hash值
bucketIndex = indexFor(hash, table.length);//通過hash值和數組的長度來計算出在數組中哪個位置
}
createEntry(hash, key, value, bucketIndex);//如果沒有擴增,則直接用傳過來的參數進行建立entry,很簡單,将添加進入的元素放桶中的第一個元素,也就是數組對應位置就是該元素,然後把之後的元素給現在元素的next,具體可以看這個方法的源碼,很簡單
}
resize()
void resize(int newCapacity) {
Entry[] oldTable = table;//将老的數組存放在oldTable中
int oldCapacity = oldTable.length;//老的數組容量
if (oldCapacity == MAXIMUM_CAPACITY) {//判斷老的容量
threshold = Integer.MAX_VALUE;//數組已經擴增到最大值了,是以将判定的标準增加到最大。
return;
}
Entry[] newTable = new Entry[newCapacity];//建立一個是原先兩倍大小的數組。
transfer(newTable, initHashSeedAsNeeded(newCapacity));//因為新數組的長度改變了,是以每個元素在新數組中的位置也會改變,是以需要将每個元素的key得到的hashcode值重新算一遍,放入新數組中
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//這裡就可以知道threshold的真正作用了,就是上面說的,作為判定是否需要擴增數組的一個标準。
}
transfer()
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//周遊老數組中的每一個桶,其實就是周遊數組的每一個位置。
while(null != e) {//周遊桶中的元素。e==null的情況是在一個桶中的最後一個元素的next為指向null,或者一開始這個桶就是空的。則需要周遊下一個桶。
Entry<K,V> next = e.next;//将e元素的下一個元素儲存到next中。
if (rehash) {//
e.hash = null == e.key ? 0 : hash(e.key);//将每個元素的hash值算出來,通過的是每個元素的key,這個算法感興趣的就點進去看。key和value為null的hash就為0,是以都在數組的第一個位置。
}
int i = indexFor(e.hash, newCapacity);//通過每個元素的hash值和所在數組的長度,計算出放在數組中哪個位置,這裡就揭示了一開始我們的疑惑,不知道通過hash值怎麼得到對應數組中的位置。
e.next = newTable[i];//每次在桶中添加新的資料,都是把新資料放在開頭,舊資料放後面,這個桶就相當于是一個棧,先進去的就在最底層。
newTable[i] = e;//将自己放入數組中的對應位置
e = next;//桶中下一個元素。
}
}
}
indexFor()
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);//通過與運算,将h的二進制,和length-1的二進制進行與運算得出的結果就是數組中的位置。
}
經過這個方法,我們可以知道以下幾點
1、構造方法中,并沒有初始化數組的大小,數組在一開始就已經被建立了,構造方法隻做兩件事情,一個是初始化加載因子,另一個是用threshold記錄下數組初始化的大小。注意是記錄。
2、什麼時候會擴增數組的大小?在put一個元素時先size>=threshold并且還要在對應數組位置上有元素,這才能擴增數組
3、對幾個重要的方法的實作了解其作用,
putForNullKey:在put時,先判斷可以是不是null值,是null值則用該方法進行處理
addEntry:增加元素的方法,其中會先判斷是不是需要擴增數組,
不需要則調用createEntry():将以擁有的四個屬性建立entry,并且做添加元素的邏輯代碼,在第一位添加,而不是在末尾追加
需要擴增調用resize():這裡面就是擴增的操作,将數組擴增為原來的兩倍。擴增後,就需要使用transfer方法進行一些元素的移動,因為數組長度變化了,原來的元素就不會呆在原來的地方不動。
indexFor:算出元素在數組中的位置索引。
Remove
//通過key删除entry并傳回其value值,
public V remove(Object key) {
//通過removeEntryForKey來完成删除功能
Entry<K,V> e = removeEntryForKey(key);
//傳回值。
return (e == null ? null : e.value);
}
removeEntryForKey:裡面代碼很簡單,就是找到key,然後将單連結清單的一些指向改一下。
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {//看hashMap中有沒有值
return null;
}
int hash = (key == null) ? 0 : hash(key);//看key是不是為null,如果為null,就直接傳回0,否則通過hash函數計算出hash值
int i = indexFor(hash, table.length);//得到在數組中的位置。
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {//開始周遊桶中所有的元素,看有沒有該key值,這個下面,prev代表前一個元素、e代表目前要檢測的元素,next代表e的後一個元素,除了第一次prev=e,其他時候都市像前面這樣。
Entry<K,V> next = e.next;//next記下一個元素
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {//判斷是該key值,
modCount++;
size--;//要删除元素了,size自減
if (prev == e)//隻有在剛開始,自己處于第一個元素的時候,這個才會等于,其他情況prev代表的是删除元素的前一個元素,
table[i] = next;//如果是第一個元素,直接把桶中第一個元素指向next,在next中儲存着原先的第二個元素,現在變為第一個元素了
else //删除的就不是第一個元素,而是之後的,由于是單連結清單,就隻需要改變一個指向引用,就是在要删除的元素之前的元素的next指向要删除的元素的next。
prev.next = next;//next:删除元素之後的一個元素,prev:删除元素的前一個元素,是以就有了這句話
e.recordRemoval(this);//一個空方法
return e;
}
prev = e;//記錄要删除元素的前一個元素
e = next;//這個就是可能要删除的元素。
}
return e;
}
get(key):通過key來找到對應的value值
//通過key獲得value,知道了hashMap的原理後,其實這些都市大同小異。
public V get(Object key) {
if (key == null)//判斷是否為null
return getForNullKey();//這個方法裡太簡單了,做兩件事情,第一,如果size=0,傳回null,反之到數組的第一個位置擷取null對應的value值,前提是有,沒有也傳回null。
Entry<K,V> entry = getEntry(key);//通過key獲得entry對象,看一下裡面是如何獲得的,我猜跟那個通過key删除元素差不多。也還是先找到對應位置,然後周遊連結清單。
return null == entry ? null : entry.getValue();//傳回
}
getEntry
//和remove(key)這個方法的邏輯一樣,但是簡單得多,因為不用删除,隻需要找到然後傳回entry對象即可
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
個人感覺其他方法都是大同小異,沒有什麼特别需要講解的了,知道了上面的原理,基本上已經沒有什麼難度。
接下來看一下hashMap的疊代器有哪些特别的沒有?
發現四個疊代器内部類都市私有的,并沒有什麼特别,HashMap對象不支援直接調用疊代器,但是可以獲得對象中所有的key集合(keySet)或者entrySet等,然後通過這些來調用疊代器獲得自己所有的key或者entry對象或者value值。
六、總結
我感覺這個hashMap花了我快一天的時間了,還是基礎太差,不懂得都要去翻閱資料。通過閱讀了HashMap源碼,看一下我們學到了什麼東西。
1、對于有些人可能還有一個疑問,就是為什麼在使用inflateTable的時候需要數組的長度大于等于 最接近指定大小的2的幂呢?
這個問題,是關于hash算法的問題了,這裡推薦一篇博文,就可以幫助你了解好這個,
http://blog.csdn.net/oqqYeYi/article/details/398310292、通過源碼的學習,hashMap是一個能快速通過key擷取到value值得一個集合,原因是内部使用的是hash查找值得方法
3、要知道hashMap是一個連結清單散列這樣一個資料結構
4、hashMap中的幾個變量要知道什麼意思,比如加載因子等知道這些,才能看得懂源碼。