Android面試刨根問底之常用源碼篇(一):Android優化,HashMap,Handler源碼分析總結

本着針對面試負責任的態度,記錄面試過程中各個知識點,而不是入門系列,如果有不懂的自行學習。

目前總結了以下幾個方面:

  • Android優化
  • HashMap分析
  • Handler源碼分析
  • OkHttp分析
  • Retrofit分析
  • 自定義View

由於篇幅原因拆分成兩個部分分享,今天就先講解前面三個方面,剩下的在後面繼續持續更新。

Android優化

大致分爲四點去回答。快、穩、小、省

1. 快

啓動快,加載快,避免卡頓

基本操作

  • 主線程不做耗時操作
  • application裏對必要的三方庫延遲初始化(延遲加載,異步加載,分佈加載)
  • 啓動白屏優化

View優化

  • View 佈局(viewstub,include,merge,層級深)
  • 複雜頁面細分優化
  • 過度繪製的優化
  • xml中無用的背景不設置
  • 控件無用屬性刪除

內存優化

  • 頁面切換,前後臺切換
  • fragment的懶加載
  • 必要的緩存
  • 空間換時間
  • 四大引用的合理使用
  • 減小不必要的內存開銷
  • 數據bean的合理定義
  • ArrayList、HashMap的使用
  • 線程池、bitmap、view的複用
  • 不用的大對象主動設置null

代碼優化

  • for循環內不定義對象
  • 使用文件IO代替數據庫
  • 自定義Drawable不在draw()裏面創建對象操作
  • 類中沒有使用到成員變量的方法可以設置static

2. 穩

穩定不崩潰,減小crash,避免anr

  • 主線程不做耗時操作
  • activity 5秒、broadcast 10秒、service 20秒
  • 資源對象及時關閉(Cursor,File)
  • Handler的處理
  • 避免內存泄露
  • crash上傳機制
  • WebView的內存泄露

3. 小

安裝包小

  • 代碼混淆(proguard)
  • 資源優化(lint)
  • 圖片優化(mipmap/webp)

4. 省

省電省流量

  • 接口定義
  • 接口緩存

性能分析工具:MAT/TracView/LeakCanary/blockCanary/MemoryMonitor/HeapViewer

HashMap分析

  • 基礎知識

    1. 可以接受null鍵和值,而Hashtable則不能
    2. 非synchronized,所以很快
    3. 存儲的是鍵值對
    4. 使用數組+鏈表的方式存儲數據
    5. 對key進行hash(散列)算法,所以順序不固定
    6. 實際使用Node存儲
  • 常量&變量


 // public class HashMap extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {}
  /**
   * The default initial capacity - MUST be a power of two.
   默認數組長度
   */
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

  /**
   * The maximum capacity, used if a higher value is implicitly specified
   * by either of the constructors with arguments.
   * MUST be a power of two <= 1<<30.
   * 數組最大長度
   */
  static final int MAXIMUM_CAPACITY = 1 << 30;

  /**
   * The load factor used when none specified in constructor.
   * 默認裝填因子
   */
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

  static class Node<K,V> implements Map.Entry<K,V> {
      final int hash;
      final K key;
      V value;
      Node<K,V> next;
  }

    /**
   * The number of key-value mappings contained in this map.
   */
  transient int size;

  /**
   * 閾值
   * The next size value at which to resize (capacity * load factor).
   *
   * @serial
   */
  // (The javadoc description is true upon serialization.
  // Additionally, if the table array has not been allocated, this
  // field holds the initial array capacity, or zero signifying
  // DEFAULT_INITIAL_CAPACITY.)
  int threshold;

  public V put(K key, V value) {
      return putVal(hash(key), key, value, false, true);
  }
  //實際存儲方法
  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {}
  //擴容方法
  final Node<K,V>[] resize() {}

  public V get(Object key) {
      Node<K,V> e;
      return (e = getNode(hash(key), key)) == null ? null : e.value;
  }
  //實際取值方法
  final Node<K,V> getNode(int hash, Object key) {}

  • 用法

    1. put(key,value)
      調用hashCode(key),使用node存儲hash,key,value,如果hashcode存在則使用鏈表存儲。

    2. get(key)
      根據key的hashcode找到Entry,然後獲取值對象,如果根據hashcode找到的是個鏈表,再去根據key.equals()判斷,鏈表中正確的節點。

  • 關於擴容

    當HashMap的大小超過了閾值(size> threshold)的時候(默認的裝填因子爲0.75,也就是說當一個map填滿了它定義容量的75%就會去擴容),HashMap大小會擴大到原來的2倍。整個過程類似於創建新的數組,將原數組的元素重新hash後放到新數組中(rehashing)。

HashMap是非同步的,所以在多線程中使用時需要注意擴容等問題

  • 相關概念
    • hashing的概念
    • HashMap中解決碰撞的方法
    • equals()和hashCode()的應用,以及它們在HashMap中的重要性
    • 不可變對象的好處
    • HashMap多線程的條件競爭
    • 重新調整HashMap的大小

參考地址:http://www.importnew.com/7099.html

以上是網上能搜到的解釋,下面是個人總結的知識點提要

如面試遇到此問題,第一步,反問面試官,您說的是哪個版本的HashMap

  • hashmap底層使用 數組+鏈表 的數據結構,實現存儲數據,使用拉鍊法解決碰撞問題。
  • map.put(key,value)的時候,內部會對key進行一次hash算法,得到一個hash值,對這個hash值&操作得到元素在數組中的位置。
  • 如果該位置沒有元素,那麼直接加入,如果發生碰撞了,那麼用拉鍊法,需要遍歷鏈表比較key和hash值,如果有就覆蓋,沒有就到表尾了,所以會插到表尾。
  • 初始容量爲16,加載因子0.75,當map添加的元素超過12個的時候會觸發擴容機制。數組的容量翻倍,已經存入的元素做rehash的操作,重新在數組中找位置存儲。
  • java8後改爲碰撞鏈表元素超過8個,用紅黑樹實現
  • java8在表尾,java7是在鏈表頭插入

思考點:
什麼情況下考慮使用SparseArray和ArrayMap替換HashMap的情況


相關面試題

1. 爲什麼HashMap的容量總是2x

從源碼中可以看到,當putVal方法中,是通過tab[i = (n - 1) & hash]得到在數組中位置的。
依稀記得當年在學校中,學到hash算法的時候,學的都是n%size運算,來確定數值在數組中的位置,而HashMap中爲什麼要用到&運算呢。

原因如下

  1. 大家都知道&運算要比%運算速度快,雖然可能是幾毫米的差別。
  2. 在n爲2x時,(n-1)&hash == hash%n

爲什麼容量總是2x
首先,Hash算法要解決的一個最大的問題,就是hash衝突,既然不能避免hash衝突,那麼就要有個好的算法解決。

而在做&運算時,如果選用非2n的數時,n-1轉換爲二進制,不能保證後幾位全爲1,這樣做在&hash的運算中,不能做到均勻分佈。違背了(n-1)&hash的初衷。

(16)10 = 24 = (10000)2
(16-1)10 =(1111)2
假設n的值非2x值,10
(10-1)10 =(1001)2
(19-1)10 =(10011)2

10011
&1111
=(11)2=(3)10

10011
&1001
=(1)2=(1)10

同樣的%運算,19%16 = 3 ,19%10 = 9。

任意一個數與(1111)2做&運算,都不會因爲(1111)2的值而影響到運算結果。

2. 如果初始化HashMap的時候定義大小爲非2x會影響到計算嗎?

答案是,肯定不會,這種情況JAVA的工程師肯定考慮到了。

源碼中我們可以看到,傳入的capacity只是影響到了threshold的值,而threshold的值還是通過tableSizeFor()確定的。

  public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

在tableSizeFor()方法中。

 static final int tableSizeFor(int cap) {
      // cap=10
        int n = cap - 1;
        // n  =9   1001
        n |= n >>> 1;
    // (1001)|(0100)=1101
        n |= n >>> 2;
    //(1101)|(0011)=1111
        n |= n >>> 4;
     // (1111)|(0000)=1111
        n |= n >>> 8;
    // (1111)|(0000)=1111
        n |= n >>> 16;
    // (1111)|(0000)=1111
      //return n+1 = (10000)=16
  //確保threshold 爲16, 2的4次冪
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

在putVal()方法中,如果第一次添加值,那麼table==null,會進入到resize()方法中,這個時候,就會用到threshold創建新的Node數組。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
      //第一次添加值,table==null; oldCap = 0;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
      //將threshold的值設置爲oldThr,下面創建table的時候用到
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
           ....
        }
        else if (oldThr > 0) 
          //通過threshold設置新數組容量
            newCap = oldThr;
        else { 
            ....
        }
        if (newThr == 0) {
           ....
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //通過threshold設置table的初始容量
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
       ....
        return newTab;
    }

通過以上操作,不論初始化HashMap的時候,傳入的容量是多少,都能保證HashMap的容量是2x

Handler源碼分析

一直在糾結一個事,因爲自己不愛看大段的文字。

自己寫總結的時候到底要不要貼上部分源碼。

後來硬着頭皮加上了,因爲源碼裏很多東西比自己寫的清楚。

RTFSC

相關概念

Handler Message MessageQueue Looper ThreadLocal

Handler機制的完整流程

  1. Message#obtain()
  2. Handler#
  3. Handler#send/post
  4. MQ#enqueueMessage() *消息的排序
  5. Looper#prepareMainLooper()
  6. Looper#prepare()
  7. ThreadLocal機制
  8. Looper#loop()
  9. MQ#next() *延遲消息的處理
  10. Handler#dispatchMessage()

Message#obtain()

message中的變量自己去看源碼,target,callback,when

從handler或者是message的源碼中都可以看到,生成Message的最終方法都是調用obtain。

ps:如果你非要用Message的構造方法,那麼看清楚他的註釋,構造方法上面的註釋寫的也很清楚,

    /** 
     * Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
     */
    public Message() {
    }

下面來分析一波obtain()方法:

  1. 爲什麼上來就是一個同步?

    任意線程都可以創建message,所以爲了維護好內部的messge池,加鎖

  2. sPool是個什麼東西

字面上看是個池子,但是從定義上看,是一個Message。爲什麼還要說成一個message池呢?因爲Message內部有個next變量,Message做成了一個鏈表的形式。這個池子怎麼存儲message呢?稍後分析源碼。

通過讀obtain()的源碼,結合鏈表的知識,很容易理解Message中Spool的原理。


    public static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;

    /**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

通過查看調用鏈,我們能夠看到在MQ中enqueueMessage調用了recycle(),而recyle中也是通過鏈表的形式對sPool進行維護。源碼簡單易懂

下面來看下sPool是怎麼維護的。

在recycleUnchecked()同樣也是加了鎖的。然後就是用鏈表的形式維護這個池子,size++

    public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                ...
            }
            return;
        }
        recycleUnchecked();
    }

    /**
     * Recycles a Message that may be in-use.
     * Used internally by the MessageQueue and Looper when disposing of queued Messages.
     */
    void recycleUnchecked() {
    ...
     synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }

    }

Handler

Handler類的源碼總共不超過1000行,並且大部分都是註釋,所以我們看該類源碼的時候,更多的是看他的註釋。靜下心來看源碼

* 構造方法
* callback對象
* dispatchMessage

Handler發送消息(send/post)

Handler發送消息的方式分爲兩種:

1.post
2.send

不論是post還是send(其他方法)方式,最終都會調用到sendMessageAtTime/sendMessageAtFrontOfQueue。執行equeueMessage,最終調用MQ#enqueueMessage(),加入到MQ中。

1. post方式

以post方式發送消息,參數基本上都是Runnable(Runnable到底是什麼,這個要搞懂)。post方式發送的的消息,都會調用getPostMessage(),將runnable封裝到Message的callbak中,調用send的相關方法發送出去。

ps:個人簡單、誤導性的科普Runnable,就是封裝了一段代碼,哪個線程執行這個runnable,就是那個線程。

2. send方式

以send方式發送消息,在衆多的重載方法中,有一類比較容易引起歧義的方法,sendEmptyMessageXxx(),這類方法並不是說沒有用到message,只是在使用的時候不需要傳遞,方法內部幫我們包裝了一個Message。另一個需要關注的點是: xxxDelayed() xxxAtTime()

1.xxxDelayed()

藉助xx翻譯,得知 delayed:延遲的,定時的,推遲 的意思,也就是說,藉助這個方法我們能做到將消息延遲發送。e.g:延遲三秒讓View消失。ps:在我年幼無知的時候,總是搞懵這個方法,不會用。

在這個方法的參數中,我們看到如果傳入的是毫秒值,那麼會在delayMillis的基礎上與SystemClock.uptimeMillis()做個加法。然後執行sendMessageAtTime()。
SystemClock.uptimeMillis() 與 System.currentTimeMillis()的區別自己去研究。

    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

2.xxxAtTime()

在這個方法就更簡單易懂了,執行的具體時間需要使用者自己去計算。

在Handler內的equeueMessage中,第一行的msg.target = this;,將handler自身賦值到msg.target,標記了這個msg從哪來,這個要注意後面會用到

MQ#enqueueMessage()

這個方法那是相當的關鍵

在此之前,我們一直鼓搗一個參數delayMillis/uptimeMillis,在這個方法裏參數名變爲了when,標明這個message何時執行,也是MQ對Message排序存儲的依據。MQ是按照when的時間排序的,並且第一個Message最先執行。

在省去了衆多目前不關心的代碼後,加上僅存的一點數據結構的知識,得到msg在MQ中的存儲形式。
mMessages位於隊列第一位置的msg,新加入到msg會跟他比較,然後找到合適的位置加入到隊列中。

ps:記得在一次面試中,面試官問到延遲消息的實現思路,我照着源碼說了一下。但是被問到:每次新加入消息,都要循環隊列,找到合適的位置插入消息,那麼怎麼保證執行效率。我不知道他這麼問是想考我優化這個東西的思路,還是他覺得我說錯了。就猶豫了一下,沒有懟回去。

 boolean enqueueMessage(Message msg, long when) {
        ...
        ...
         synchronized (this) {
            ...
            ...
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
            ...

            ...
        }
        return true;
    }

以上幾步,我們只是將要執行的msg加入到了隊列中。接下來分析下什麼時候執行msg。

再接再厲,馬上就看到暑光了。

Looper#prepareMainLooper()

藉助十幾年英語學習積累下來的詞彙量,加上我出色的看源碼能力。看懂了這個方法的註釋及Android系統在哪裏執行了此方法。

面試被問到怎麼在子線程創建Looper?

仔細看註釋。Initialize the current thread as a looper....See also: {@link #prepare()}

這個方法,作爲開發人員不需要調用它,但是作爲一個高級技工還是要多少了解一點的,系統在三個位置調用了此方法,但是我只關心了AndroidThread這個類,AndroidThread是個啥,自己去看吧。

    /**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

Looper#prepare()

面試的時候經常被問到一個線程可以有多個looper嗎?

看源碼註釋就得到了答案。
throw new RuntimeException("Only one Looper may be created per thread");
怎麼保證每個線程只有一個looper呢?這裏用到了ThreadLocal。

在自己創建的子線程中,如果想創建Looper,那麼只需要調用Looper.prepare(),就會爲當前線程創建一個looper了。

    private static void prepare(boolean quitAllowed) {  
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

ThreadLocal機制

ThreadLocal是個什麼東西呢,他是個複雜的機制,畢竟從JAVA1.2就加入了機制,保證了每個線程自己的變量....

本人簡單的、帶有誤導性的科普是:

類似一個Map,key是當前線程id,value就是你要保存的值

一定要自己深入瞭解該機制

Looper#loop()

這個方法也很關鍵,消息能夠執行,起了很大作用。雖然個人感覺能看的代碼很少,但是都很精煉啊。

  1. 獲取looper,得到MQ
  2. 循環MQ得到可執行的msg
  3. 通過msg自身,去到他該去的地方msg.target.dispatchMessage(msg);
  4. recycleUnchecked(),維護Message池

ps:曾經年少的我一度認爲Looper就是主線程,完全因爲這個loop()方法,當時看到在AndroidThread#main()中執行了Looper.loop(),而學過JAVA的都知道main()裏面,如果沒有耗時、子線程等其他操作,基本上執行到最後一行,就結束了。

但是爲什麼APP起來了,main()裏面那麼幾行代碼執行結束後,沒有死掉呢。就是因爲loop()裏面有個for(;;),當MQ中沒有msg,那麼會一直循環下去。

現在想來,還是太年輕了。這個只是一方面原因,其他線程也會調用Looper.prepare(),爲自己創建looper,然後執行Looper.loop(),循環自己的MQ。

發現還是要多瞭解,多學習。

MQ#next()

這個方法負責把隊列中的msg取出來,給到looper去執行。

這個方法也是一個for(;;),當取到第一個msg的時候,如果沒有到他該執行的時間,那麼就等着,一直等,死等。得到可以執行的msg後,給到Looper。裏面還有些native的方法,大家自己去看next()源碼吧。

Handler#dispatchMessage()

在Looper#loop()中MQ#next()得到了msg,有這麼一行msg.target.dispatchMessage(msg);,在之前講到了這個target是發送msg的那個handler(多個handler的情況下區分)。根據不同情況,對msg進行分發。如果有callback對象(post方式發送消息,或者new Handler(runnable)),就去執行Runnable.run()。其他情況回調到handleMessage(),在創建handler的地方處理這個msg。

 /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

以上就是本人對Handler的總結。

寫了這麼多,已經累癱在辦公桌前,啥都不想幹了。這可能是在高考語文結束後,想的最多的一次文字。

最後囉嗦一句
RTFSC(Read The Fucking Source Code)

爲什麼android設計只能UI線程更新UI

  1. 解決多線程併發的問題。多個線程更新UI可能發生併發問題,如果在多個線程中加鎖,會導致程序頁面有可能非常卡頓
  2. 提高界面更新的性能問題
  3. 架構設計的簡單,因爲android中封裝了所有更新UI的操作,在開發中只需要在非UI中發送一個消息,就可以更新UI,對於開發人員來說節省了不少時間.

相關面試題

  1. 子線程Looper和Handler

  2. 延遲消息怎麼處理

  3. ThreadLocal作用

  4. 自己實現Handler機制

  5. for (;;) 與while(true) 區別

     看了些文章,自己動手試了試,.class文件。一毛錢的區別都沒有。
     有人說根據編譯器不同會有差別,在我目前的能力認知範圍內沒差別。
    
    
  6. 同步消息屏障

Message next() {
        ......
        synchronized (this) {
          // Try to retrieve the next message.  Return if found.
          final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
           ......
            }
      }

在view繪製的時候,post到MQ的消息是不會被執行的,優先執行繪製時候的異步消息。

7.IdleHandler的實現原理

Message next() {
        ......
        synchronized (this) {
          // Try to retrieve the next message.  Return if found.
           ......
            // If first time idle, then get the number of idlers to run.
              // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
          // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
            }
      }

面試大廠複習路線

多餘的話就不講了,接下來將分享面試的一個複習路線,如果你也在準備面試但是不知道怎麼高效複習,可以參考一下我的複習路線,有任何問題也歡迎一起互相交流,加油吧!

這裏給大家提供一個方向,進行體系化的學習:

1、看視頻進行系統學習

前幾年的Crud經歷,讓我明白自己真的算是菜雞中的戰鬥機,也正因爲Crud,導致自己技術比較零散,也不夠深入不夠系統,所以重新進行學習是很有必要的。我差的是系統知識,差的結構框架和思路,所以通過視頻來學習,效果更好,也更全面。關於視頻學習,個人可以推薦去B站進行學習,B站上有很多學習視頻,唯一的缺點就是免費的容易過時。

另外,我自己也珍藏了好幾套視頻,有需要的我也可以分享給你。

2、進行系統梳理知識,提升儲備

客戶端開發的知識點就那麼多,面試問來問去還是那麼點東西。所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度。so,出去面試時先看看自己複習到了哪個階段就好。

系統學習方向:

  • 架構師築基必備技能:深入Java泛型+註解深入淺出+併發編程+數據傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO

  • Android高級UI與FrameWork源碼:高級UI晉升+Framework內核解析+Android組件內核+數據持久化

  • 360°全方面性能調優:設計思想與代碼質量優化+程序性能優化+開發效率優化

  • 解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網絡訪問框架設計+RXJava響應式編程框架設計+IOC架構設計+Android架構組件Jetpack

  • NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發

  • 微信小程序:小程序介紹+UI開發+API操作+微信對接

  • Hybrid 開發與Flutter:Html5項目實戰+Flutter進階

知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。

3、讀源碼,看實戰筆記,學習大神思路

“編程語言是程序員的表達的方式,而架構是程序員對世界的認知”。所以,程序員要想快速認知並學習架構,讀源碼是必不可少的。閱讀源碼,是解決問題 + 理解事物,更重要的:看到源碼背後的想法;程序員說:讀萬行源碼,行萬種實踐。

主要內含微信 MMKV 源碼、AsyncTask 源碼、Volley 源碼、Retrofit源碼、OkHttp 源碼等等。

4、面試前夕,刷題衝刺

面試的前一週時間內,就可以開始刷題衝刺了。請記住,刷題的時候,技術的優先,算法的看些基本的,比如排序等即可,而智力題,除非是校招,否則一般不怎麼會問。

關於面試刷題,我個人也準備了一套系統的面試題,幫助你舉一反三:

總結

改變人生,沒有什麼捷徑可言,這條路需要自己親自去走一走,只有深入思考,不斷反思總結,保持學習的熱情,一步一步構建自己完整的知識體系,纔是最終的制勝之道,也是程序員應該承擔的使命。

以上內容均免費分享給大家,需要完整版的朋友,點這裏可以看到全部內容。或者關注主頁掃描加 微信 獲取。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章