原始碼的魅力 - ArrayMap的工作原理

Nichoool發表於2017-10-03

ArrayMap的工作原理(Android 7.1原始碼)

其他相關文章

  1. 原始碼的魅力 - ArrayDeque 的工作原理
  2. 原始碼的魅力 - HashMap 的工作原理
  3. 原始碼的魅力 - TreeMap 的工作原理
  4. GankIo又一個ReactNative客戶端

簡介

鑑於HashMap的記憶體使用率太低,而記憶體又是Android裝置系統中極其寶貴的資源,所以Google開發出ArrayMap來一定條件下取代HashMap (幾乎平時的開發都可以使用這個結構,而不用HashMap)。

初始化


    int[] mHashes;
    Object[] mArray;

   public ArrayMap(int capacity, boolean identityHashCode) {
       mIdentityHashCode = identityHashCode;

       if (capacity < 0) {
           mHashes = EMPTY_IMMUTABLE_INTS;
           mArray = EmptyArray.OBJECT;
       } else if (capacity == 0) {
           mHashes = EmptyArray.INT;
           mArray = EmptyArray.OBJECT;
       } else {
           allocArrays(capacity);
       }
       mSize = 0;
   }複製程式碼

ArrayMap的內部主要依靠mHashes以及mArray,建構函式中可以看出和HashMap一樣一上來建立時並不會分配記憶體空間。此處的mhashes就是存放HashCode的陣列,mArray時存放key與Value的陣列。

put 方法

    @Override
    public V put(K key, V value) {
        final int hash;
        int index;
        if (key == null) {
            hash = 0;
            index = indexOfNull();
        } else {
            hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
            index = indexOf(key, hash);
        }
        if (index >= 0) {
            index = (index<<1) + 1;
            final V old = (V)mArray[index];
            mArray[index] = value;
            return old;
        }

        index = ~index;
        if (mSize >= mHashes.length) {
            final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
                    : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

            if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);

            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n);

            if (mHashes.length > 0) {
                if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
            }

            freeArrays(ohashes, oarray, mSize);
        }

        if (index < mSize) {
            if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
                    + " to " + (index+1));
            System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }

        mHashes[index] = hash;
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
    }複製程式碼
  • 當key為空時直接通過indexOfNull獲取
    index
  • 當key不為空時,hash = key.hashCode獲取hash值,並且通過indexOf方法來獲取位置(indexOf與indexOfNull將在下面詳細分析)
  • 如果返回的index大於零,直接通過index = (index << 1)+ 1來獲取到mArray陣列的索引直接替換資料(由於mArray存放的資料是一個key然後一個Value形式的, index* 2就是key的位置,再加一就是value的位置)
  • 當沒有找到key的位置時,返回的index是負數,通過取反來獲取到新插入的位置。
  • 當mHash列表滿了的時候,執行擴容方法allocArrays() allocArrays與下面的freeArrays方法將在後面進行分析
  • 將老資料分配到分配到新陣列中。
  • 由於此時需要在index位置新插入資料,所以需要將mHashcodes以及mArray中的資料向右移動,在mHashcodes陣列中騰出一個位置,mArray中騰出兩個位置。
  • 將新資料插入,mSize自增

結構圖
結構圖

indexOf以及indexOfNull方法

由於indexOfNull其實就是hash引數為0的特殊版本,基本上是一樣的所以就不贅述了。

  //獲取索引值
  int indexOf(Object key, int hash) {
      final int N = mSize;

      // Important fast case: if nothing is in here, nothing to look for.
      if (N == 0) {
          return ~0;
      }

      int index = ContainerHelpers.binarySearch(mHashes, N, hash);

      // If the hash code wasn't found, then we have no entry for this key.
      if (index < 0) {
          return index;
      }

      // If the key at the returned index matches, that's what we want.
      if (key.equals(mArray[index<<1])) {
          return index;
      }

      // Search for a matching key after the index.
      int end;
      for (end = index + 1; end < N && mHashes[end] == hash; end++) {
          if (key.equals(mArray[end << 1])) return end;
      }

      // Search for a matching key before the index.
      for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
          if (key.equals(mArray[i << 1])) return i;
      }

      // Key not found -- return negative value indicating where a
      // new entry for this key should go.  We use the end of the
      // hash chain to reduce the number of array entries that will
      // need to be copied when inserting.
      return ~end;
  }

  static int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;

        while (lo <= hi) {
            final int mid = (lo + hi) >>> 1;
            final int midVal = array[mid];

            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // value found
            }
        }
        return ~lo;  // value not present
    }複製程式碼
  • 當之前沒有資料時,則返回0的取反值。
  • 通過二分法在ContainerHelpers.binarySearch()中查詢資料
  • 在binarySearch函式當數值並沒有的時候,會返回lo的取反值,這個lo此時指向的位置是Value插入的位置。(其實這個取反以及其他位置的取反就是整個ArrayMap中巧妙的地方之一,binarySearch通過返回取反值既做到了資料不存在時返回負數,又提供了資料新插入的位置,一舉多得。
  • 當返回的index為負數時,直接返回索引值。
  • 如果key正好是在mArray的2*index的位置的資料,則直接返回index
  • 如果還是不對,則通過mHashs向下查詢,如果存在相同的hash值,並且對應的key也相等則返回index值。
  • 反之則向下查詢。
  • 如果還是沒有找到,直接返回end(end 是 等於index + 1)的值取反值

總結:indexOf函式返回正數時是存在key值的mHashCodes陣列的索引值,否則返回負值就是在mHashCode新插入位置的索引的取反值,再取反就可以得到插入的位置

allocArrays以及freeArrays函式


    private void allocArrays(final int size) {
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        if (size == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCache != null) {
                    final Object[] array = mTwiceBaseCache;
                    mArray = array;
                    mTwiceBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;
                }
            }
        } else if (size == BASE_SIZE) {
            synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;
                }
            }
        }

        mHashes = new int[size];
        mArray = new Object[size<<1];
    }

    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
        if (hashes.length == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCacheSize < CACHE_SIZE) {
                    array[0] = mTwiceBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mTwiceBaseCache = array;
                    mTwiceBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
                            + " now have " + mTwiceBaseCacheSize + " entries");
                }
            }
        } else if (hashes.length == BASE_SIZE) {
            synchronized (ArrayMap.class) {
                if (mBaseCacheSize < CACHE_SIZE) {
                    array[0] = mBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mBaseCache = array;
                    mBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                            + " now have " + mBaseCacheSize + " entries");
                }
            }
        }
    }複製程式碼

ArrayMap通過兩個陣列mTwiceBaseCache以及mBaseCache兩個陣列來提供快取記憶體空間,但只有在陣列大小是4與8時發揮作用。

final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
        : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);複製程式碼

前面的put中擴充套件陣列部分程式碼中,當小於4時擴充套件成4,小於8時擴充套件成8,否則增加一半的 mSize大小。
allocArrays與freeArrays在快取部分是相對應的,作用是將原先的那部分記憶體空間儲存住,不然垃圾回收器回收掉。

總結

ArrayMap在記憶體使用率上比HashMap高不少,但是插入速度在資料很多時會很慢,所以ArrayMap在資料量少時(千以內)比較適用,否則還是要使用HashMap。


更多好文章請關注微信公眾號【Android技術棧】,獵豹移動大牛將提供給你更好的技術心得,公眾號才剛剛起步希望大家多多支援。

Android技術棧
Android技術棧

相關文章