• HashMap源码分析——基于jdk1.7


    前言:相信不管在生产过程中还是面试过程中,HashMap出现的几率都非常的大,因此有必要对其源码进行分析,但要注意的是jdk1.8对HashMap进行了大量的优化,因此笔者会根据不同版本对HashMap进行分析,首先我们来看jdk1.7中HashMap的原理。

    注:本文jdk源码版本为jdk1.7.0_80。


    1.从demo入手

     1 public class HashMapTest {
     2 
     3     public static void main(String[] args) {
     4 
     5         String key_Aa = "Aa";
     6         String key_BB = "BB";
     7 
     8         // 注意这里的hashCode值
     9         System.out.println("key_Aa hashCode=" + key_Aa.hashCode());
    10         System.out.println("key_BB hashCode=" + key_BB.hashCode());
    11 
    12         Map<String, String> hashMap = new HashMap<String, String>();
    13 
    14         hashMap.put(key_Aa,"Aa");
    15         hashMap.put(key_Aa,"Aa");
    16 //        hashMap.put(key_BB,"Aa");
    17         System.out.println(hashMap);
    18     }
    19 }

    先直接看运行结果:

    然后打开16行代码的注释,再次运行:

    通过以上两组运行结果,我们可以得出如下结论:

    #1.不同内容的字符串,其hashCode值可能是相等的。

    #2.HashMap在进行put操作时,key相同时(注:hashCode和内容都是一样的),进行了覆盖;key不同时(注:hashCode不同】或【hashCode相同,内容不同】)进行了插入(直接插入table上,或进行链表式插入数据)操作。

    #3.对于HashMap来说,重要的是key,而不是value,value相当于key的一个附属值

    有了以上结论,接下来我们对其源码进行分析就比较有针对性了。

    2.源码分析

    2.1 put操作

     1 public V put(K key, V value) {
     2         // 如果table为空,则初始化
     3         if (table == EMPTY_TABLE) {
     4             inflateTable(threshold);
     5         }
     6         if (key == null)  // 从这里可看出HashMap是可以插入null值的,key,value都可以为null
     7             return putForNullKey(value);
     8         int hash = hash(key); // 对key进行hash操作
     9         int i = indexFor(hash, table.length); // 找出key的hash值在table中对应的位置
    10         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    11             Object k;
    12             // 如果该位置上元素的hash值与插入元素的hash值相等,并且key也相等或者内容相等,这里就进行覆盖操作 这里与demo中结论相吻合
    13             // 注意这里的写法,比较巧妙
    14             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    15                 V oldValue = e.value;
    16                 e.value = value;
    17                 e.recordAccess(this);
    18                 return oldValue;
    19             }
    20         }
    21         
    22         // 插入新元素,并且modCount++,modCount表示HashMap修改的次数
    23         modCount++;
    24         addEntry(hash, key, value, i);
    25         return null;
    26 }

    分析:

    #1.HashMap的元素是存储在table中,table的类型为Entry数组,先了解下Entry结构:

    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next; // 存储下一个元素的值,这不就是一个链表吗,因此HashMap的底层数组结构为数组+链表的形式
            int hash;
    
            /**
             * Creates new entry.
             */
            // 从Entry的构造函数可以得出结论,在创建一个new Entry的时候,会将old Entry(在table位置上的元素)放在new Entry的next位置,形成链表。从其构造形式可以得出结论:在插入元素时采用的是头插入,新元素都是放在链表头(table上)的,将原来的头元素放在新元素的next位置,形成一个新的链表 
            Entry(int h, K k, V v, Entry<K,V> n) {
                value = v;// 当前新元素value
                next = n; // next表示原来的old Entry
                key = k; // 当前新元素key
                hash = h;
            }
    .......
    .......
    .......
    }

    要点:头插法

    #2.inflateTable方法,初始化table。

     1    private void inflateTable(int toSize) {
     2         // Find a power of 2 >= toSize
     3         /**
     4          * HashMap的容量都是2的n次方的,这里表示计算出比传入参数最小的2的n次方的值
     5          */
     6         /**
     7          *   HashMap的默认容量:
     8          *   The default initial capacity - MUST be a power of two.
     9          *   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    10          */
    11         int capacity = roundUpToPowerOf2(toSize);
    12          
    13         /**
    14          *   扩容阈值,通过HashMap容量与扩容因子计算出来
    15          *   默认扩容因子为0.75
    16          *   The load factor used when none specified in constructor.
    17          *   static final float DEFAULT_LOAD_FACTOR = 0.75f;
    18          */
    19         threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    20         table = new Entry[capacity];
    21         // 该函数主要在扩容中使用,判断是否需要重新计算hash值
    22         initHashSeedAsNeeded(capacity);
    23     }

    #3.从put函数可以看出HashMap是可以存储key为null的元素的,由于value为key的附属值,所以value也可以为null

     1  private V putForNullKey(V value) {
     2         // 循环找出key=null的元素,然后将其值覆盖,从这里可以看HashMap中key=null时,是不会形成链表的
     3         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     4             if (e.key == null) {
     5                 V oldValue = e.value;
     6                 e.value = value;
     7                 e.recordAccess(this);
     8                 return oldValue;
     9             }
    10         }
    11         // 如果没有key为null的元素,则增加一个元素
    12         modCount++;
    13         addEntry(0, null, value, 0);
    14         return null;
    15     }

    #4.在putForNullKey中出现了addEntry函数,因此这里对其进行分析。

     1    /**
     2      * HashMap增加元素
     3      *
     4      * @param hash key的hash值
     5      * @param key  key值
     6      * @param value value值
     7      * @param bucketIndex 插入元素在table中的位置
     8      */
     9     void addEntry(int hash, K key, V value, int bucketIndex) {
    10        // 如果当前HashMap的容量大于扩容阈值并且当前插入元素的位置在table上对应的值不为null
    11        // 注意这里的table[bucketIndex]非常重要,如果当前位置上为空,是不需要扩容的       
    12        if ((size >= threshold) && (null != table[bucketIndex])) {
    13             // 扩容,将新容量扩容为原来的2倍
    14             resize(2 * table.length);
    15             // 再次计算key的hash值,然后找出元素在新table中对应的位置
    16             hash = (null != key) ? hash(key) : 0;
    17             bucketIndex = indexFor(hash, table.length);
    18         }
    19         // 创建新元素
    20         createEntry(hash, key, value, bucketIndex);
    21     }  

    重点:在判断是否需要扩容时,需要判断当前位置在table上的元素是否为null,只有不为null才进行扩容。

    #5.resize函数,这里先不忙分析,先看createEntry函数。

     1    /**
     2      * 创建元素
     3      *
     4      * @param hash key的hash值
     5      * @param key  key值
     6      * @param value value值
     7      * @param bucketIndex 插入元素在table中的位置
     8      */
     9     void createEntry(int hash, K key, V value, int bucketIndex) {
    10         // 取出要插入位置上的元素,可能为null,也可能有具体元素
    11         Entry<K, V> e = table[bucketIndex];
    12         // 在插入位置上直接创建新元素,注意这里传入的参数e,在Entry构造函数中会放在next中,从而形成链表,从这里也可以看出在出现hash碰撞的时候,HashMap在插入元素的时候,采用的是头插法
    13         table[bucketIndex] = new Entry<>(hash, key, value, e);
    14         // size++,表示容量增加1个
    15         size++;
    16     }

    重点:插入元素时采用的头插法。因为采用头插法,所以在hash碰撞的时候,才会出现demo中的结果,注意理解,这里交相呼应。

    #6.在put方法中有两个方法值得我们注意hash(Object k)和indexFor(int h, int length)。

     1 final int hash(Object k) {
     2         int h = hashSeed;
     3         // 如果使用了再次hash,并且key的类型为String,则直接使用String的hash算法返回其hash值
     4         if (0 != h && k instanceof String) {
     5             return sun.misc.Hashing.stringHash32((String) k);
     6         }
     7         // 如果走到这里h可能为0或者为1,再次异或上k的hashCode,如果h为1,表示再hash,则这里的h可能会±1,h为0的时候,h就表示k的hashCode
     8         h ^= k.hashCode();
     9 
    10         // This function ensures that hashCodes that differ only by
    11         // constant multiples at each bit position have a bounded
    12         // number of collisions (approximately 8 at default load factor).
    13         // 这里进行两次hash主要是为了最大可能的解决hash碰撞,防止低位不变,而高位变化时,产生hash碰撞
    14         h ^= (h >>> 20) ^ (h >>> 12);
    15         return h ^ (h >>> 7) ^ (h >>> 4);
    16     }
    17     

    这里为什么要进行了两次hash,通过如下计算过程可以大致了解一下:

    假设目前hashMap的容量为16
    -------------------------------------------------------------------
    h_1:                    0101 1000 1101 0111 0011 1110 1001 1011
    h_1>>>20:               0000 0000 0000 0000 0000 0101 1000 1101 
    h_1>>>12:               0000 0000 0000 0101 1000 1101 0111 0011
    h_1>>>20^h_1>>>12:      0000 0000 0000 0101 1000 1000 1111 1110
    h_1:                    0101 1000 1101 0111 0011 1110 1001 1011
    h_1^h_1>>>20^h_1>>>12:  0101 1000 1101 0010 1011 0110 0100 0101
    h_1>>>7:                0000 0000 1011 0001 1010 0101 0110 1100
    h_1>>>4:                0000 0101 1000 1101 0010 1011 0110 0100 
    h_1^h_1>>>7:            0101 1000 0110 0011 0001 0011 0010 1001
    h_1^h_1>>>7^h_1>>>4     0101 1101 1110 1110 0011 1000 0100 1101
    &table.length-1:        0000 0000 0000 0000 0000 0000 0000 1111
    result:                 0000 0000 0000 0000 0000 0000 0000 1101=13
    ---------------------------------------------------------------------
    h_2:                    0101 1000 1101 1111 0011 1110 1001 1011
    h_2>>>20:               0000 0000 0000 0000 0000 0101 1000 1101 
    h_2>>>12:               0000 0000 0000 0101 1000 1101 1111 0011
    h_2>>>20^h_2>>>12:      0000 0000 0000 0101 1000 1000 1111 1110
    h_2:                    0101 1000 1101 1111 0011 1110 1001 1011
    h_2^h_2>>>20^h_2>>>12:  0101 1000 1101 1010 1011 0110 0110 0101
    h_2>>>7:                0000 0000 1011 0001 1011 0101 0110 1100
    h_2>>>4:                0000 0101 1000 1101 1010 1011 0110 0110
    h_2^h_2>>>7:            0101 1000 0110 1011 0000 0011 0000 1001
    h_2^h_2>>>7^h_2>>>4     0101 1101 1110 0110 1010 1000 0110 1111
    &table.length-1:        0000 0000 0000 0000 0000 0000 0000 1111
    result:                 0000 0000 0000 0000 0000 0000 0000 1111=15

    分析:

    注意上述计算过程中h_1和h_2只有高位中有一位不同,其余全都相同。

    如果不采用二次hash这种方式,而是直接和table.length-1进行与操作,得到的结果都是11,造成hash碰撞;采用二次hash的方式,将高位加入运算,对key的hashCode进行了扰动计算,防止低位不变,而高位变化时造成hash碰撞,从而尽可能的减少hash碰撞。

    再看indexFor函数:

     1 static int indexFor(int h, int length) {
     2         // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
     3         // 这里采用位运算来进行操作,求出元素在table中的位置,相当于mod运算,但是位运算效率更高
     4         // 从这里可以了解到为什么HashMap的容量必须为2的n次方,因为2的n次方-1的二进制永远全部是1,这样会减少hash碰撞
     5         /**
     6          *     h & (table.length-1)          hash                    table.length-1
     7          *
     8          *        8 & (15-1):                0100         &              1110           =   0100
     9          *
    10          *        9 & (15-1):                0101         &              1110           =   0100
    11          *        
    12          *       ----------------------------------------------------------------------------------
    13          *
    14          *        8 & (16-1):                0100        &              1111            =   0100
    15          *
    16          *        9 & (16-1):                0101        &              1111            =   0101
    17          */
    18         return h & (length-1);
    19     }

    重点:从这里反推出:为什么HashMap的容量为什么会是2的n次方,因为这样可以尽量减少hash碰撞

    #7.接下来进入HashMap的扩容函数resize(int newCapacity):

     1 void resize(int newCapacity) {
     2         Entry[] oldTable = table;
     3         int oldCapacity = oldTable.length;
     4         // 判断是否允许扩容
     5         if (oldCapacity == MAXIMUM_CAPACITY) {
     6             threshold = Integer.MAX_VALUE;
     7             return;
     8         }
     9         
    10         // 创建一个新的Entry数组,容量为原来的2倍
    11         Entry[] newTable = new Entry[newCapacity];
    12         // 扩容主要函数transfer
    13         transfer(newTable, initHashSeedAsNeeded(newCapacity));
    14         table = newTable;
    15         // 更新扩容阈值 
    16         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    17     }

    注意:HashMap扩容后容量变为原来的2倍,在resize中的核心函数就是transfer:

     1    /**
     2      * 扩容核心函数
     3      *
     4      * @param newTable  新的Entry数组
     5      * @param rehash    是否需要再次hash
     6      */
     7     void transfer(Entry[] newTable, boolean rehash) {
     8         int newCapacity = newTable.length; 
     9         // 循环原table
    10         for (Entry<K, V> e : table) {
    11             // 当元素为null时,循环结束
    12             while (null != e) {
    13                 // 存储当前元素的next,因为可能存在链表结构,所以必须存储next元素
    14                 Entry<K, V> next = e.next;
    15                 // 判定是否需要再次对key进行hash操作
    16                 if (rehash) {
    17                     e.hash = null == e.key ? 0 : hash(e.key);
    18                 }
    19                 // 得到元素在newTable中的位置
    20                 int i = indexFor(e.hash, newCapacity); 
    21                  /*
    22                   * #1.将newTabe[i]位置上的元素放入e的next位置
    23                   * #2.将e放入newTable[i]位置
    24                   * #3.将next元素赋值给e,继续进行循环
    25                   * 注意:这里会将原链表(如果存在)进行反序
    26                   * 原:3->7->5
    27                   * 第一次循环:newTable[i]=3,其next=null
    28                   * 第二次循环:newTable[i]=7,其next=newTable[i]=3(上一次的)
    29                   * 第三次循环:newTable[i]=5,其next=newTable[i]=7,next.next=3
    30                   * 最终结果:5->7->3,进行了反序                  
    31                   */
    32                 e.next = newTable[i];
    33                 newTable[i] = e;
    34                 e = next;
    35             }
    36         }
    37     }

    注意:transfer函数为扩容的核心函数,并且会将原table上的链表(如果存在)进行反序,这里也是HashMap在多线程中线程不安全的体现,具体线程不安全体现分析传送门:HashMap线程不安全的体现

    至此HashMap的put源码已经分析完毕,下面继续分析HashMap中的其他源码。

    2.2 get操作

    1   public V get(Object key) {
    2         // 如果key=null,则通过getForNullKey函数返回值,这里也侧面反映出HashMap的key可以为null的
    3         if (key == null)
    4             return getForNullKey();
    5         // 通过key取得具体的元素
    6         Entry<K,V> entry = getEntry(key);
    7         // 返回值,这里也反映出HashMap的值也可以是null的
    8         return null == entry ? null : entry.getValue();
    9     }

    分析:

    通过HashMap的get函数,可得出结论:HashMap的key可以为null,其value也可以为null,因为value相当于key的一个附属值,既然key可以为null,value当然也可以。

    #1.getForNullKey函数

     1 private V getForNullKey() {
     2         // 如果HashMap中还未put元素,则直接返回null
     3         if (size == 0) {
     4             return null;
     5         }
     6         // 在table中循环,找到key为null的元素,直接返回去value
     7         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     8             if (e.key == null)
     9                 return e.value;
    10         }
    11         // 如果在table中未找到,则直接返回null
    12         return null;
    13     }

    getForNullKey函数逻辑简单,通过以上注释,基本上可以理解清楚。

    #2.getEntry(Object key) 函数

     1 final Entry<K,V> getEntry(Object key) {
     2        // 如果HashMap中没有元素,则直接返回null       
     3        if (size == 0) {
     4             return null;
     5         }
     6         // 注意这里再次判断了key是否为null,然后通过hash算法计算出hashCode
     7         int hash = (key == null) ? 0 : hash(key);
     8         // 由于可能table上可能存在链表,所以这里要从table[index]开头进行循环
     9         for (Entry<K,V> e = table[indexFor(hash, table.length)];
    10              e != null;
    11              e = e.next) {
    12             Object k;
    13             // 当元素的hashCode和key相同时,直接返回元素
    14             if (e.hash == hash &&
    15                 ((k = e.key) == key || (key != null && key.equals(k))))
    16                 return e;
    17         }
    18         // 如果未找到,则返回null
    19         return null;
    20     }

    要点:通过key找元素的时候,由于HashMap的底层结构是数组+链表的形式,所以这里要进行循环

    2.3 其他重要函数

    #1.removeEntryForKey(Object key),该函数为remove的核心函数。

     1 final Entry<K,V> removeEntryForKey(Object key) {
     2         // 如果HashMap中没有元素,则直接返回null
     3         if (size == 0) {
     4             return null;
     5         }
     6         // 计算key的hash值
     7         int hash = (key == null) ? 0 : hash(key);
     8         // 找到key在table上的位置
     9         int i = indexFor(hash, table.length);
    10         // prev记录table[i]的前一个元素,初始时等于table[i]
    11         Entry<K,V> prev = table[i];
    12         // e记录要删除元素开始处,初始时等于table[i]
    13         Entry<K,V> e = prev;
    14         
    15         // 循环寻找要删除的元素
    16         while (e != null) {
    17             // 由于table中可能存在链表,所以需要记录一下next的值。
    18             Entry<K,V> next = e.next;
    19             Object k;
    20             // 当e的hashCode和key与入参相同时,则找到要删除的元素
    21             if (e.hash == hash &&
    22                 ((k = e.key) == key || (key != null && key.equals(k)))) {
    23                 modCount++; // 修改次数加1
    24                 size--;     // HashMap包含元素减1
    25                 // 因为初始时,prev=e,也就是说要移除的元素就是table[i]上,则直接将table[i]指向e.next,后面的都不需要移动
    26                 if (prev == e) 
    27                     table[i] = next;
    28                 else
    29                     prev.next = next; // 如果prev!=null,prev记录的是当前准备删除元素的前一个元素,这里直接将prev.next存储e的next值,就把e踢出了,prev和e的后一个元素形成了链表
    30                 e.recordRemoval(this);
    31                 return e;
    32             }
    33             // 如果上述未找到,则prev=e,记录当前准备删除元素的前一个元素
    34             prev = e;
    35             // e赋值为next继续删除操作
    36             e = next;
    37         }
    38 
    39         return e;
    40     }

    分析:

    removeEntryForKey函数用得非常巧妙,只修改了一个节点的next值,就进行了删除操作,注意理解具体的删除逻辑。

    为了更直观的了解remove的过程,笔者这里通过源码的调试来展示其具体过程:

     1  public static void main(String[] args) {
     2 
     3         String key_Aa = "Aa";
     4         String key_BB = "BB";
     5 
     6         // 注意这里的hashCode值
     7         System.out.println("key_Aa hashCode=" + key_Aa.hashCode());
     8         System.out.println("key_BB hashCode=" + key_BB.hashCode());
     9 
    10         Map<String, String> hashMap = new HashMap<String, String>();
    11 
    12         hashMap.put(key_Aa, "Aa");
    13         hashMap.put(key_Aa, "Aa");
    14         hashMap.put(key_BB, "Aa");
    15         hashMap.remove(key_BB);
    16 //        hashMap.remove(key_Aa);
    17         System.out.println(hashMap);
    18 
    19     }

    将上述代码Debug:

    注意:由于“Aa”和“BB”的hashCode相等,所以此时HashMap是链式存储,顺序为key:BB->Aa

    直接进入remove函数内部:

    此时找到了删除元素,并且prev与e是相等的,所以直接将table[i]=next就移除了e了。

    将上述代码15行注释,打开16行,再次进入remove内部:

    分析:

    由于table[i]头上放置的是key为BB的元素,所以在第一次比较的时候,直接跳过,此次prev=BB,e=Aa,从这里可以看出,prev存储的是当前将要删除元素的前一个元素,所以删除时直接使用prev.next=next就踢出e了。

    通过以上Debug过程,删除元素的过程应该非常清晰了。

    #2.HashMap的fail-fast机制。

    还是通过代码入手:

     1 package com.developer.java7.collections.hashmap;
     2 
     3 import java.util.HashMap;
     4 import java.util.Map;
     5 
     6 /**
     7  * @author: developer
     8  * @date: 2019/3/3 9:29
     9  * @description: hashmap测试
    10  */
    11 
    12 public class HashMapTest {
    13 
    14     public static void main(String[] args) {
    15 
    16         String key_Aa = "Aa";
    17         String key_BB = "BB";
    18         String key_Cc = "Cc";
    19         String key_Dd = "Dd";
    20 
    21         // 注意这里的hashCode值
    22         System.out.println("key_Aa hashCode=" + key_Aa.hashCode());
    23         System.out.println("key_BB hashCode=" + key_BB.hashCode());
    24 
    25         Map<String, String> hashMap = new HashMap<String, String>();
    26 
    27         hashMap.put(key_Aa, "Aa");
    28         hashMap.put(key_Aa, "Aa");
    29         hashMap.put(key_BB, "Aa");
    30         hashMap.put(key_Cc, "Cc");
    31         hashMap.put(key_Dd, "Dd");
    32         for (String key : hashMap.keySet()) {
    33             if (key_Dd.equals(key)) {
    34                 hashMap.remove(key);
    35             }
    36         }
    37         System.out.println(hashMap);
    38 
    39     }
    40 
    41 }

    运行结果如下:

    将代码稍微修改一下:

     1  public static void main(String[] args) {
     2 
     3         String key_Aa = "Aa";
     4         String key_BB = "BB";
     5         String key_Cc = "Cc";
     6         String key_Dd = "Dd";
     7 
     8         // 注意这里的hashCode值
     9         System.out.println("key_Aa hashCode=" + key_Aa.hashCode());
    10         System.out.println("key_BB hashCode=" + key_BB.hashCode());
    11 
    12         Map<String, String> hashMap = new HashMap<String, String>();
    13 
    14         hashMap.put(key_Aa, "Aa");
    15         hashMap.put(key_Aa, "Aa");
    16         hashMap.put(key_BB, "Aa");
    17         hashMap.put(key_Cc, "Cc");
    18         hashMap.put(key_Dd, "Dd");
    19         for (String key : hashMap.keySet()) {
    20             if (key_Cc.equals(key)) {
    21                 hashMap.remove(key);
    22             }
    23         }
    24         System.out.println(hashMap);
    25 
    26     }

    运行结果如下:

    出现异常了,接着修改代码,依次移除key_Aa,key_BB都会报该异常,只有删除最后一个元素不会报异常,这是HashMap的fail-fast机制。

    为什么删除最后一个元素不会报错呢,这里简要分析一下:

    在循环初始化Iterator时,会先记录HashMap修改的次数。

    每次去获取nextEntry元素时,会判断modCount是否和expectedModCount是否相等,如果不相等则直接抛出异常。因为expectedModCount只是初始化的时候保存了modCount的值,后续modCount修改后并不会更新expectedModCount,所以remove时抛出异常。

    但是为什么删除最后一个元素不抛异常呢?

    这里的next已经为null了,不会往下走了,所以不抛异常,但是如果next不为null,也会抛异常的。

    HashMap本身线程不安全,所以出现fail-fast也是正常的,在面对并发修改时,迭代器很快就会抛出异常,从而确定修改方法。

    总结

    至此,HashMap源码基本分析完了,后续如果还有一些重要的点,再加上。这里将源码分析中的重点再次总结一下:

    #1.HashMap无序,通过源码调试的截图可知,因为HashMap是根据key的hash值来进行存储的,从这里也可以确定HashMap是无序的。

    #2.HashMap线程不安全。

    #3.HashMap可以存储key=null和value=null的值。


    by Shawn Chen,2019.03.05,下午。

  • 相关阅读:
    ( 转)移动端H5页面之iphone6的适配
    谨慎设置iScroll4的useTransform属性,他会导致scrollToElement方法表现异常
    (转)配置Apache服务器,使浏览器访问无缓存
    html DIV元素左右偏移方法,偏移后默认宽度仍浏览器宽度一致
    Content Security Policy(CSP)简介(转)
    隐式打开Activity——Intent设置(如何打开)和Intent-fileter配置(怎么能被打开)
    最近使用iScroll遇到的一些问题及最后的解决方法
    (转)CSS3 Media Queries
    自定义checkbox和radio
    三只松鼠卖坚果
  • 原文地址:https://www.cnblogs.com/developer_chan/p/10464109.html
Copyright © 2020-2023  润新知