1. 程式人生 > >成為Android高階工程師——你所要知道的那些“原理”

成為Android高階工程師——你所要知道的那些“原理”

日常程式設計中,我們一定用到各種資料結構、第三方框架等。通常我們只需要知道這些輪子有什麼用、如何用就可以了,但要達到高階工程師、資深工程師的檔次,就一定會涉及到“原理”問題,無論是從造輪子還是解決疑難雜症的考慮,公司一定都會希望他們花了高價聘請的“高階”人才,具有原理知識。下面我就列舉一些常見的“原理”,供大家參考。

一、Handler實現原理

或許,這是被問到最多一個原理吧。

Handler是Android用於執行緒與執行緒間通訊的一套機制。通常被拿來在子執行緒完成耗時操作後,與主執行緒通訊更新UI的操作。

Handler實現原理依賴Message\MessageQueue\Looper這三者。

1、Message:訊息物件。例項化的最好方式是使用Message.obtain或者Handler.obtainMessage從物件池中獲取message例項。

2、MessageQueue:訊息佇列。存放message的集合,並由Looper例項來分發裡面的訊息物件。

3、Looper:訊息迴圈。通過Looper.prepare()獲取一個訊息迴圈,並通過呼叫Looper.loop方法無限迴圈獲取並分發MessageQueue中的訊息。(Tip:如果在主執行緒中建立handler例項,是不需要呼叫prepare、loop方法的,因為ActivityThread建立時就已經初始化了Looper)。

實現流程:Handle通過sendMessage或者post方法把Message傳送到MessageQueue中,Looper通過MessageQueue的next方法獲取訊息並分發,如果通過post傳送的,則執行callback回撥,如果通過send傳送的,則執行重寫的handMessage方法。

二、HashMap內部原理

hashMap應該是考察java資料結構中最常被問到的一種資料型別。

資料結構中有陣列和連結串列來實現對資料對儲存,這兩者是兩個極端。陣列儲存區間是連續的,佔用記憶體嚴重,但查詢效率高;而連結串列儲存區間是離散的,佔用記憶體較小,但時間複雜度高,查詢複雜。

有沒有結合兩者特性,既定址容易、也插入刪除簡單的資料結構呢?答案是“有”,雜湊表(Hash table)。雜湊表最常用的一種實現方式是——拉鍊法,可以把它看作“連結串列的陣列”。

Hashmap儲存資料的容器也是一個線性陣列,它具有一個靜態內部類Node,資料結構如下:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //對Key計算的hash值
        final K key; //Key
        V value; //value
        Node<K,V> next; //連結串列指向的下一個Node
}

儲存時:

int index = (length - 1) & hash(key); // hash值與Node長度取模,得到陣列下標
Node[index] = value;

取值時:

int index = (length - 1) & hash(key); // hash值與Node長度取模,得到陣列下標
return Node[index];

其中的hash方法在java8中實現如下:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

這個“擾動”函式的價值是:將hash值右移16位(剛好32bit的一半),然後讓自身的上半區與下半區做“亦或”操作,為的是加大低位的隨機性,再與Node長度取模,做為下標,可以有效減少碰撞次數。

hash(圖:擾動函式)

那麼,兩個key的hash值取模得到相同的index,會不會把前一個node覆蓋呢?

這裡就用到了hashmap的鏈式結構了,Node裡面有一個next屬性,指向下一個Node。例如,進來一個鍵值對A,對keyhash取模得到index=0,則Node[0]=A,有進來一個鍵值對B,得到對index也為0,hashmap這樣處理,B.next=A,Node[0]=B,這時又進來一個C,同樣index=0,則C.next=B,Node[0]=C。我們發現 陣列中總是存放最新的一個Node元素

 (圖:陣列的連結串列)

HashMap是如何根據Key取出value的呢? 我們看一段程式碼

public V get(Object key) {
        int hash = hash(key.hashCode());
        //先定位到陣列元素,再遍歷該元素處的連結串列
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                do {
                    if (e.hash == hash &&((k = e. key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
}

三、ButterKnife實現原理

大部分Android開發應該都知道@JakeWharton 大神的ButterKnife註解庫,使用這個庫我們可以不用寫很多無聊的findViewById()和setOnClickListener()等程式碼。那麼這個庫是如何實現的呢?

可能很多人都覺得ButterKnife在bind(this)方法執行的時候通過反射獲取Activity中所有的帶有@Bind註解的屬性並且獲得註解中的R.id.xxx值,然後通過反射拿到Activity.findViewById()方法獲取View。這是一個註解庫的原始實現方式,很大的缺點就是在Activity執行時大量使用反射會影響App的執行效能,造成卡頓以及生成很多臨時Java物件更容易觸發GC。

ButterKnife顯然沒有使用這種方式,它用了Java Annotation Processing技術,就是在Java程式碼編譯成Java位元組碼的時候就已經處理了@Bind、@OnClick(ButterKnife還支援很多其他的註解)這些註解了。

Java Annotation Processing

用於編譯時掃描和解析Java註解的工具 ,你可以你定義註解,並且自己定義解析器來處理它們。

Annotation processing是在編譯階段執行的,它的原理就是讀入Java原始碼,解析註解,然後生成新的Java程式碼。新生成的Java程式碼最後被編譯成Java位元組碼。(圖:註解解析的過程)


Butterknife工作流程

當你編譯你的Android工程時,ButterKnife工程中ButterKnifeProcessor類的process()方法會執行以下操作: 

開始它會掃描Java程式碼中所有的ButterKnife註解@Bind、@OnClick、@OnItemClicked等 ,當它發現一個類中含有註解時,ButterKnifeProcessor會生成一個Java類,名字$ViewBinder,這個類實現了ViewBinder介面,這個ViewBinder類中包含了所有對應的程式碼,比如@Bind註解對應findViewById(), @OnClick對應了view.setOnClickListener()等等。

最後當Activity啟動ButterKnife.bind(this)執行時,ButterKnife會去載入對應的ViewBinder類呼叫它們的bind()方法。

public class ConfirmInfoActivity$$ViewInjector {
    ...
    public static void bind(Finder finder, final com.huicent.ui.ConfirmInfoActivity target, Object source) {
        View view;
        view = finder.findRequiredView(source, 2131297974, "field 'mTvName'");
        target.mTvName = (android.widget.TextView) view;
        view = finder.findRequiredView(source, 2131297526, "field 'mFlightType'");
        target.mFlightType = (android.widget.TextView) view;
        view = finder.findRequiredView(source, 2131296512, "field 'mChangeBtn' and method 'onClick'");
        target.mChangeBtn = (android.widget.Button) view;
        view.setOnClickListener(
          new android.view.View.OnClickListener() {
            @Override public void onClick(
              android.view.View p0
            ) {
              target.onClick(p0);
            }
          });
        ...
    }

在上面的過程中可以看到,為什麼你用@Bind、@OnClick等註解標註的屬性或方法必須是public或protected的。因為ButterKnife是通過target.this.editText來注入View的 

為什麼要這樣呢?答案就是效能。如果你把View設定成private,那麼框架必須通過反射來注入View,不管現在手機的CPU處理器變得多快,如果有些操作會影響效能,那麼是肯定要避免的,這就是ButterKnife與其他注入框架的不同。

Butterknife對效能到底有沒有影響?

對於使用ButterKnife註解的類,都會生成實現ViewBinder介面名稱原類名+$$ViewBinder的相應輔助類。這個過程處於編譯期間,也就是我們APT在編譯時處理註解生成的。由此可知,對執行時的效能,這個階段是沒有影響的。

編譯期生成的輔助類,想要完成繫結View,還需要一個bind的過程。原始碼如下:

String clsName = cls.getName();
            if(!clsName.startsWith("android.") && !clsName.startsWith("java.")) {
                try {
                    Class<?> injector = Class.forName(clsName + "$$ViewInjector");
                    inject = injector.getMethod("inject", new Class[]{ButterKnife.Finder.class, cls, Object.class});
                    if(debug) {
                        Log.d("ButterKnife", "HIT: Class loaded injection class.");
                    }
                } catch (ClassNotFoundException var4) {
                    if(debug) {
                        Log.d("ButterKnife", "Not found. Trying superclass " + cls.getSuperclass().getName());
                    }

                    inject = findInjectorForClass(cls.getSuperclass());
                }

                INJECTORS.put(cls, inject);
                return inject;
            } else {
                if(debug) {
                    Log.d("ButterKnife", "MISS: Reached framework class. Abandoning search.");
                }

                return NO_OP;
            }
該方法有兩個影響效能的地方,就是Class.forName和injector.getMethod這兩個方法。

通過原理分析,結論顯而易見。ButterKnife對效能有一定的影響,並且引入了更多的類和方法,增加了安裝包的大小。但是,對開發效率的提升也是顯而易見的,尤其是配合AS外掛的使用。

四、Volley工作原理

Volley 是 Google 推出的輕量級 Android 非同步網路請求框架和圖片載入框架。在 Google I/O 2013 大會上釋出。其適用場景是資料量小,通訊頻繁的網路操作。

我們知道,把一個Request add進RequestQueue後,Volley就開始工作了,那麼Volley是如何工作的?且讓我們從頭分析。

1 - 請求佇列(RequestQueue)的建立

建立請求佇列的工作是從Volley.newRequestQueue開始的,這個方法內部會呼叫RequestQueue的構造器,同時指定一些基本配置,如快取策略為硬碟快取(DiskBasedCache),http請求方式為HttpURLConnection(level>9)和HttpClient(level<9),預設執行緒池大小(4)。最後,呼叫RequestQueue#start啟動請求佇列。

//volley.java
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
          ... ...
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                // Prior to Gingerbread, HttpUrlConnection was unreliable.
                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }
        Network network = new BasicNetwork(stack);
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();
        return queue;
    }

我們來看看RequestQueue的構造器

public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize]; //預設執行緒池大小為4
        mDelivery = delivery; //結果分發器 new ExecutorDelivery(new Handler(Looper.getMainLooper())) 將結果返回給主執行緒(根據程式碼中使用了Handler和UI執行緒的Looper大家就應該能猜到了),並處理回撥事件。
    }

RequestQueue的start方法執行了什麼

public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();
        // Create network dispatchers (and corresponding threads) up to the pool size.
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

邏輯很簡單,建立了CacheDispatcher和4個NetworkDispatcher個物件,然後分別啟動之。這個CacheDispatcher和NetworkDispatcher都是Thread的子類,其中CacheDispatcher處理走快取的請求,而4個NetworkDispatcher處理走網路的請求。到此,RequestQueue任務完成了,後面的工作就交由dispatch處理。圖:RequestQueue的建立

2 - 請求的新增(RequestQueue.add)

步驟如下:(1)將Request加入mCurrentRequests集合 (2)為請求加上序號 (3)判斷是否需要快取請求,如果不需要,直接加入網路請求佇列 (4)如果有相同請求正在處理,則加入到相同請求的等待佇列中,否則加入快取佇列。

public Request add(Request request) {
        // Tag the request as belonging to this queue and add it to the set of current requests.
        request.setRequestQueue(this);
        synchronized (mCurrentRequests) {
            mCurrentRequests.add(request);
        }
        // Process requests in the order they are added.
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");
        // If the request is uncacheable, skip the cache queue and go straight to the network.
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }
        // Insert request into stage if there's already a request with the same cache key in flight.
        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                Queue<Request> stagedRequests = mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                    stagedRequests = new LinkedList<Request>();
                }
                stagedRequests.add(request);
                mWaitingRequests.put(cacheKey, stagedRequests);
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
                }
            } else {
                // Insert 'null' queue for this cacheKey, indicating there is now a request in
                // flight.
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }
    }

3 - 請求的處理

處理Request是通過CacheDispatcher和NetworkDispatcher完成的,他們的run方法通過不斷的迴圈從各自的佇列中取出請求,進行處理,交給ResponseDelivery。

CacheDispatcher快取分發器的run方法,處理過程如下圖:


大體邏輯是這樣的,首先從佇列中取出請求,看其是否已被取消,若是則返回,否則繼續向下走。接著從硬碟快取中通過快取的鍵找到值(Cache.Entry),如果找不到或者過期了,那麼將此請求加入網路請求佇列。如果沒有過期,那麼通過request.parseNetworkResponse方法將硬碟快取中的資料封裝成Response物件。最後進行新鮮度判斷,如果不需要重新整理,那麼呼叫ResponseDelivery結果分發器的postResponse分發結果。否則先將結果返回,再將請求交給網路請求佇列進行重新整理。

NetworkDispatcher實現邏輯如下:


4 - 結果的分發與處理

請求結果的分發處理是由ResponseDelivery實現類ExecutorDelivery完成的,ExecutorDelivery是在RequestQueue的構造器中被建立的,並且綁定了UI執行緒的Looper。

public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }

ExecutorDelivery內部有個自定義Executor,它僅僅是封裝了Handler,所有待分發的結果最終會通過handler.post方法交給UI執行緒。

public ExecutorDelivery(final Handler handler) {
        // Make an Executor that just wraps the handler.
        mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };
    }

執行結果分發的是ResponseDeliveryRunnable,我們看看其原始碼:

private class ResponseDeliveryRunnable implements Runnable {
        private final Request mRequest;
        private final Response mResponse;
        private final Runnable mRunnable;
        public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
            mRequest = request;
            mResponse = response;
            mRunnable = runnable;
        }
        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            // If this request has canceled, finish it and don't deliver.
            if (mRequest.isCanceled()) {
                mRequest.finish("canceled-at-delivery");
                return;
            }
            // Deliver a normal response or error, depending.
            if (mResponse.isSuccess()) {
                mRequest.deliverResponse(mResponse.result);
            } else {
                mRequest.deliverError(mResponse.error);
            }
            // If this is an intermediate response, add a marker, otherwise we're done
            // and the request can be finished.
            if (mResponse.intermediate) {
                mRequest.addMarker("intermediate-response");
            } else {
                mRequest.finish("done");
            }
            // If we have been provided a post-delivery runnable, run it.
            if (mRunnable != null) {
                mRunnable.run();
            }
       }
}


這裡我們看到了request.deliverResponse被呼叫了,這個方法通常會回撥Listener.onResponse。

到這裡,整個volley框架的主線就結束了!!

最後,貼上一幅圖概括了整個volley框架的結構組成。


五、OkHttp原理

OkHttp是一個高效的HTTP庫,它與Volley的工作流程非常相似,總體設計圖如下:


通過Diapatcher不斷從RequestQueue中取出請求(Call),根據是否已快取呼叫Cache或 Network這兩類資料獲取介面之一,從記憶體快取或是伺服器取得請求的資料。該引擎有同步和非同步請求,同步請求通過Call.execute()直接返 回當前的Response,而非同步請求會把當前的請求Call.enqueue新增(AsyncCall)到請求佇列中,並通過回撥(Callback) 的方式來獲取最後結果。

六、Glide原理

七、EventBus原理


相關推薦

成為Android高階工程師——知道那些原理

日常程式設計中,我們一定用到各種資料結構、第三方框架等。通常我們只需要知道這些輪子有什麼用、如何用就可以了,但要達到高階工程師、資深工程師的檔次,就一定會涉及到“原理”問題,無論是從造輪子還是解決疑難雜症的考慮,公司一定都會希望他們花了高價聘請的“高階”人才,具有原理知識。下

[Android]Android記憶體洩漏知道的一切(翻譯)

以下內容為原創,歡迎轉載,轉載請註明 來自天天部落格:http://www.cnblogs.com/tiantianbyconan/p/7235616.html Android記憶體洩漏你所要知道的一切 原文:https://blog.aritraroy.in/everything-

Android基礎篇之Android快速入門--必須知道的基礎

1. Activity的理解: 2. Intent的理解 關於IntentFilter 3. Intent的使用:(建立、攜帶資料、讀取資料) 1.建立:      顯式意圖: Intent intent = new Inten

面試知道的:MySQL儲存過程

儲存過程簡介SQL語句需要先編譯然後執行,而儲存過程(Stored Procedure)是一組為了完成特定功能的SQL語句集,經編譯後儲存在資料庫中,使用者通過指定儲存過程的名字並給定引數(如果該儲存過程帶有引數)來呼叫執行它。儲存過程是可程式設計的函式,在資料庫中建立並儲存

Android關於銷燬應該知道

finalize()用途何在 五步看懂: 我們都瞭解初始化的重要性,當常常會忘記同樣也重要的清理工作。在Java中有垃圾回收器負責(GC)回收無用的物件佔據的記憶體資源。 但是也有特殊情況:假定你的物件(並非使用new)獲得一塊“特殊”的記憶體區域。為

Android Service完全解析,關於服務知道的一切(下)

並且 無法 數據類型 界面 其它 wid logcat listen 程序崩潰 文章轉載至:http://blog.csdn.net/guolin_blog/article/details/9797169 這是郭霖寫的.......就是寫 "第一行代碼"的那個厲害人物,大

Android關於Canvas知道的和不知道的一切

在一年的Android自學中,Canvas一直是我能避且避的類,甚至不惜封裝自己的繪相簿來替代它。 如今回首,虐我千萬次的Canvas也不過如此,靜下心看看,其實也沒有想象中的那麼糟糕。 就像曾經等級30的我去打點等級40的副本(Canvas)非常吃力,現在等級50的我回來吊打它一樣。 所以朋友,遇到承

Android關於Path知道的和不知道的一切

零、前言 1.canvas本身提供了很多繪製基本圖形的方法,普通繪製基本滿足 2.但是更高階的繪製canvas便束手無策,但它的一個方法卻將圖形的繪製連線到了另一個次元 3.下面進入Path的世界,[注]:本文只說Path,關於繪製只要使用Canvas.drawPath(Path,Paint)即可 4

Android關於Paint知道的和不知道的一切

零、前言: 1.曾經也算半個藝術家,深知筆的重要性與複雜性 2.Android裡的Paint設定項好多基本上都是setXXX,getXXX,很多文字相關的內容都在Paint裡 3.主要由畫筆常規配置,畫筆型別、畫筆特效(線效,著色,濾色)、畫筆文字 4.本文暫時還無法覆蓋Paint的所有API,能用的

Android關於Color知道的和不知道的一切

零、前言 1.做安卓的大多應該對顏色不太敏感,畢竟咱是敲程式碼的,顏色有設計師呢。 2.不過作為一名在大學被顏色薰(陶)過四年的人,對顏色多少還是挺親切的(雖然當時挺討厭的) 3.紀念也好,記錄也罷,為它寫篇總結也理所應當 4.如果你覺得並不需要了解關於顏色的知識,那你可以將本文當做一篇科普文(出去跟

Android Service完全解析,關於服務知道的一切(上)(筆記)

參考原文:Android Service完全解析,關於服務你所需知道的一切(上) Service的基本用法 然後新建一個MyService繼承自Service,並重寫父類的onCreate()、onStartCommand()和onDestroy()方法, 可以看到,在Sta

Android drawable微技巧 知道的drawable的那些細節

                     轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/50727753 好像有挺久時間沒更新部落格了,最近我為了準備下一個系列的部落格,也是花了很長的時間研讀原始碼。很遺憾的是,下一個系列的部落格我可能還要再

小白怎麼成為全職自媒體人?必須知道的幾點!

之前看有大部分學員都是用下班時間或者是課餘時間來操作和運營自媒體,也有學員問到是不是放棄工作來全職做自媒體就能賺到更多錢呢?(在校學生不建議放棄課業來全身心投入。) 首先我們得搞明白一個順序,就是那些全職的自媒體人究竟是先放棄工作再來鑽研操作自媒體的,還是已經鑽

Android Fragment完全解析,關於碎片知道的一切

我們都知道,Android上的介面展示都是通過Activity實現的,Activity實在是太常用了,我相信大家都已經非常熟悉了,這裡就不再贅述。但是Activity也有它的侷限性,同樣的介面在手機上顯示可能很好看,在平板上就未必了,因為平板的螢幕非常大,手機的介面放在平板上

Android任務和返回棧完全解析,細數那些知道的細節

本篇文章主要內容來自於Android Doc,我翻譯之後又做了些加工,英文好的朋友也可以直接去讀原文。任務和返回棧一個應用程式當中通常都會包含很多個Activity,每個Activity都應該設計成為一個具有特定的功能,並且可以讓使用者進行操作的元件。另外,Activity之

Android Scroller完全解析,關於Scroller知道的一切

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/48719871 2016大家新年好!這是今年的第一篇文章,那麼應CSDN工作人員的建議,為了能給大家帶來更好的閱讀體驗,我也是將部落格換成了寬屏

知道Android Studio 除錯技巧

Android Studio目前已經成為開發Android的主要工具,用熟了可謂相當順手。作為開發者,除錯並發現bug,進而解決,可是我們的看家本領。正所謂,工欲善其事必先利其器,和其他開發工具一樣,如Eclipse、Idea,Android Studio也為我們提供了強大的除

Android drawable微技巧,知道的drawable的那些細節

好像有挺久時間沒更新部落格了,最近我為了準備下一個系列的部落格, 也是花了很長的時間研讀原始碼。很遺憾的是,下一個系列的部落格我可能還要再過一段時間才能寫出來,那麼為了不至於讓大家等太久,今天就給大家更新一篇單篇的文章,講一講android drawable方面的微技巧。 話說

Android 那些知道的Bitmap物件詳解

我們知道Android系統分配給每個應用程式的記憶體是有限的,Bitmap作為消耗記憶體大戶,我們對Bitmap的管理稍有不當就可能引發OutOfMemoryError,而Bitmap物件在不同的Android版本中存在一些差異,今天就給大家介紹下這些差異,並提供一些在使用B

知道Android Studio除錯技巧

Android Studio目前已經成為開發Android的主要工具,用熟了可謂相當順手。作為開發者,除錯並發現bug,進而解決,可是我們的看家本領。正所謂,工欲善其事必先利其器,和其他開發工具一樣,如Eclipse、Idea,Android Studio也