工作7年,面試失敗101次,10月騰訊我涼在一面,沒臉見人:連這些Android核心問題都不懂?

2020-10-18 20:00:36

做了7年Android,面試過上百家公司,我終於總結出一番道理,那就是空有技術,不懂面試,照樣找不到好工作!

90%的面試者涼在一面!

從難度上說,第一輪面試不會很難,題目大多是面試官一早想好,主要只考核基礎問題。但可怕的是在整個面試過程中,就屬這個環節刷掉的人最多!

很多面試者的技術能力很強,卻被無限放大了某個小問題,對最新技術的稍不敏感,對基礎知識的微小疏漏,都可能是致命的。

如果能夠在面試之前,對面試題目已經做好充分理解,先一步做好你的正確答案,一定能讓面試官對你有不一樣的評價!

想快速拿到大廠的高薪offer,你要先清楚大廠會考你什麼問題,我分析了眾多企業對於Android開發工程師的需求,總結了這套出鏡率最高的面試題!

一、常規知識點

1、 Android類載入器

在Android開發中,不管是外掛化還是元件化,都是基於Android系統的類載入器ClassLoader來設計的。只不過Android平臺上虛擬機器器執行的是Dex位元組碼,一種對class檔案優化的產物,傳統Class檔案是一個Java原始碼檔案會生成一個.class檔案,而Android是把所有Class檔案進行合併、優化,然後再生成一個最終的class.dex,目的是把不同class檔案重複的東西只需保留一份,在早期的Android應用開發中,如果不對Android應用進行分dex處理,那麼最後一個應用的apk只會有一個dex檔案。

Android中常用的類載入器有兩種,DexClassLoader和PathClassLoader,它們都繼承於BaseDexClassLoader。區別在於呼叫父類別構造器時,DexClassLoader多傳了一個optimizedDirectory引數,這個目錄必須是內部儲存路徑,用來快取系統建立的Dex檔案。而PathClassLoader該引數為null,只能載入內部儲存目錄的Dex檔案。所以我們可以用DexClassLoader去載入外部的apk檔案,這也是很多外掛化技術的基礎。

2、 Service

理解Android的Service,可以從以下幾個方面來理解:

  • Service是在main Thread中執行,Service中不能執行耗時操作(網路請求,拷貝資料庫,大檔案)。
  • 可以在xml中設定Service所在的程序,讓Service在另外的程序中執行。
  • Service執行的操作最多是20s,BroadcastReceiver是10s,Activity是5s。
  • Activity通過bindService(Intent,ServiceConnection,flag)與Service繫結。
  • Activity可以通過startService和bindService啟動Service。

IntentService

IntentService是一個抽象類,繼承自Service,內部存在一個ServiceHandler(Handler)和HandlerThread(Thread)。IntentService是處理非同步請求的一個類,在IntentService中有一個工作執行緒(HandlerThread)來處理耗時操作,啟動IntentService的方式和普通的一樣,不過當執行完任務之後,IntentService會自動停止。另外可以多次啟動IntentService,每一個耗時操作都會以工作佇列的形式在IntentService的onHandleIntent回撥中執行,並且每次執行一個工作執行緒。IntentService的本質是:封裝了一個HandlerThread和Handler的非同步框架。

2.1、生命週期示意圖

Service 作為 Android四大元件之一,應用非常廣泛。和Activity一樣,Service 也有一系列的生命週期回撥函數,具體如下圖。

通常,啟動Service有兩種方式,startService和bindService方式。

2.2、startService生命週期

當我們通過呼叫了Context的startService方法後,我們便啟動了Service,通過startService方法啟動的Service會一直無限期地執行下去,只有在外部呼叫Context的stopService或Service內部呼叫Service的stopSelf方法時,該Service才會停止執行並銷燬。

onCreate

onCreate: 執行startService方法時,如果Service沒有執行的時候會建立該Service並執行Service的onCreate回撥方法;如果Service已經處於執行中,那麼執行startService方法不會執行Service的onCreate方法。也就是說如果多次執行了Context的startService方法啟動Service,Service方法的onCreate方法只會在第一次建立Service的時候呼叫一次,以後均不會再次呼叫。我們可以在onCreate方法中完成一些Service初始化相關的操作。

onStartCommand

onStartCommand: 在執行了startService方法之後,有可能會呼叫Service的onCreate方法,在這之後一定會執行Service的onStartCommand回撥方法。也就是說,如果多次執行了Context的startService方法,那麼Service的onStartCommand方法也會相應的多次呼叫。onStartCommand方法很重要,我們在該方法中根據傳入的Intent引數進行實際的操作,比如會在此處建立一個執行緒用於下載資料或播放音樂等。

public @StartResult int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) {
}

當Android面臨記憶體匱乏的時候,可能會銷燬掉你當前執行的Service,然後待記憶體充足的時候可以重新建立Service,Service被Android系統強制銷燬並再次重建的行為依賴於Service中onStartCommand方法的返回值。我們常用的返回值有三種值,START_NOT_STICKYSTART_STICKYSTART_REDELIVER_INTENT,這三個值都是Service中的靜態常數。

START_NOT_STICKY

如果返回START_NOT_STICKY,表示當Service執行的程序被Android系統強制殺掉之後,不會重新建立該Service,當然如果在其被殺掉之後一段時間又呼叫了startService,那麼該Service又將被範例化。那什麼情境下返回該值比較恰當呢?如果我們某個Service執行的工作被中斷幾次無關緊要或者對Android記憶體緊張的情況下需要被殺掉且不會立即重新建立這種行為也可接受,那麼我們便可將 onStartCommand的返回值設定為START_NOT_STICKY。舉個例子,某個Service需要定時從伺服器獲取最新資料:通過一個定時器每隔指定的N分鐘讓定時器啟動Service去獲取伺服器端的最新資料。當執行到Service的onStartCommand時,在該方法內再規劃一個N分鐘後的定時器用於再次啟動該Service並開闢一個新的執行緒去執行網路操作。假設Service在從伺服器獲取最新資料的過程中被Android系統強制殺掉,Service不會再重新建立,這也沒關係,因為再過N分鐘定時器就會再次啟動該Service並重新獲取資料。

START_STICKY

如果返回START_STICKY,表示Service執行的程序被Android系統強制殺掉之後,Android系統會將該Service依然設定為started狀態(即執行狀態),但是不再儲存onStartCommand方法傳入的intent物件,然後Android系統會嘗試再次重新建立該Service,並執行onStartCommand回撥方法,但是onStartCommand回撥方法的Intent引數為null,也就是onStartCommand方法雖然會執行但是獲取不到intent資訊。如果你的Service可以在任意時刻執行或結束都沒什麼問題,而且不需要intent資訊,那麼就可以在onStartCommand方法中返回START_STICKY,比如一個用來播放背景音樂功能的Service就適合返回該值。

START_REDELIVER_INTENT

如果返回START_REDELIVER_INTENT,表示Service執行的程序被Android系統強制殺掉之後,與返回START_STICKY的情況類似,Android系統會將再次重新建立該Service,並執行onStartCommand回撥方法,但是不同的是,Android系統會再次將Service在被殺掉之前最後一次傳入onStartCommand方法中的Intent再次保留下來並再次傳入到重新建立後的Service的onStartCommand方法中,這樣我們就能讀取到intent引數。只要返回START_REDELIVER_INTENT,那麼onStartCommand重的intent一定不是null。如果我們的Service需要依賴具體的Intent才能執行(需要從Intent中讀取相關資料資訊等),並且在強制銷燬後有必要重新建立執行,那麼這樣的Service就適合返回START_REDELIVER_INTENT。

onBind

Service中的onBind方法是抽象方法,所以Service類本身就是抽象類,也就是onBind方法是必須重寫的,即使我們用不到。在通過startService使用Service時,我們在重寫onBind方法時,只需要將其返回null即可。onBind方法主要是用於給bindService方法呼叫Service時才會使用到。

onDestroy

onDestroy: 通過startService方法啟動的Service會無限期執行,只有當呼叫了Context的stopService或在Service內部呼叫stopSelf方法時,Service才會停止執行並銷燬,在銷燬的時候會執行Service回撥函數。

2.3、bindService生命週期

bindService方式啟動Service主要有以下幾個生命週期函數:

onCreate():

首次建立服務時,系統將呼叫此方法。如果服務已在執行,則不會呼叫此方法,該方法只呼叫一次。

onStartCommand():

當另一個元件通過呼叫startService()請求啟動服務時,系統將呼叫此方法。

onDestroy():

當服務不再使用且將被銷燬時,系統將呼叫此方法。

onBind():

當另一個元件通過呼叫bindService()與服務繫結時,系統將呼叫此方法。

onUnbind():

當另一個元件通過呼叫unbindService()與服務解綁時,系統將呼叫此方法。

onRebind():

當舊的元件與服務解綁後,另一個新的元件與服務繫結,onUnbind()返回true時,系統將呼叫此方法。

3、fragemnt

3.1、建立方式

(1)靜態建立

首先我們需要建立一個xml檔案,然後建立與之對應的java檔案,通過onCreatView()的返回方法進行關聯,最後我們需要在Activity中進行設定相關引數即在Activity的xml檔案中放上fragment的位置。

 <fragment
        android:name="xxx.BlankFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </fragment>

(2)動態建立

動態建立Fragment主要有以下幾個步驟:

  1. 建立待新增的fragment範例。
  2. 獲取FragmentManager,在Activity中可以直接通過呼叫 getSupportFragmentManager()方法得到。
  3. 開啟一個事務,通過呼叫beginTransaction()方法開啟。
  4. 向容器內新增或替換fragment,一般使用repalce()方法實現,需要傳入容器的id和待新增的fragment範例。
  5. 提交事務,呼叫commit()方法來完成。

3.2、Adapter對比

FragmnetPageAdapter在每次切換頁面時,只是將Fragment進行分離,適合頁面較少的Fragment使用以儲存一些記憶體,對系統記憶體不會多大影響。

FragmentPageStateAdapter在每次切換頁面的時候,是將Fragment進行回收,適合頁面較多的Fragment使用,這樣就不會消耗更多的記憶體

3.3、Activity生命週期

Activity的生命週期如下圖:

(1)動態載入:

動態載入時,Activity的onCreate()呼叫完,才開始載入fragment並呼叫其生命週期方法,所以在第一個生命週期方法onAttach()中便能獲取Activity以及Activity的佈局的元件;

(2)靜態載入:

1.靜態載入時,Activity的onCreate()呼叫過程中,fragment也在載入,所以fragment無法獲取到Activity的佈局中的元件,但為什麼能獲取到Activity呢?

2.原來在fragment呼叫onAttach()之前其實還呼叫了一個方法onInflate(),該方法被呼叫時fragment已經是和Activity相互結合了,所以可以獲取到對方,但是Activity的onCreate()呼叫還未完成,故無法獲取Activity的元件;

3.Activity的onCreate()呼叫完成是,fragment會呼叫onActivityCreated()生命週期方法,因此在這兒開始便能獲取到Activity的佈局的元件;

3.4、與Activity通訊

fragment不通過建構函式進行傳值的原因是因為橫屏切換的時候獲取不到值。

Activity向Fragment傳值:

Activity向Fragment傳值,要傳的值放到bundle物件裡; 在Activity中建立該Fragment的物件fragment,通過呼叫setArguments()傳遞到fragment中; 在該Fragment中通過呼叫getArguments()得到bundle物件,就能得到裡面的值。

Fragment向Activity傳值:
第一種:

在Activity中呼叫getFragmentManager()得到fragmentManager,,呼叫findFragmentByTag(tag)或者通過findFragmentById(id),例如:

FragmentManager fragmentManager = getFragmentManager();

Fragment fragment = fragmentManager.findFragmentByTag(tag);

第二種:

通過回撥的方式,定義一個介面(可以在Fragment類中定義),介面中有一個空的方法,在fragment中需要的時候呼叫介面的方法,值可以作為引數放在這個方法中,然後讓Activity實現這個介面,必然會重寫這個方法,這樣值就傳到了Activity中

Fragment與Fragment之間是如何傳值的:
第一種:

通過findFragmentByTag得到另一個的Fragment的物件,這樣就可以呼叫另一個的方法了。

第二種:

通過介面回撥的方式。

第三種:

通過setArguments,getArguments的方式。

3.5、api區別

add

一種是add方式來進行show和add,這種方式你切換fragment不會讓fragment重新重新整理,只會呼叫onHiddenChanged(boolean isHidden)。

replace

而用replace方式會使fragment重新重新整理,因為add方式是將fragment隱藏了而不是銷燬再建立,replace方式每次都是重新建立。

commit/commitAllowingStateLoss

兩者都可以提交fragment的操作,唯一的不同是第二種方法,允許丟失一些介面的狀態和資訊,幾乎所有的開發者都遇到過這樣的錯誤:無法在activity呼叫了onSaveInstanceState之後再執行commit(),這種異常時可以理解的,介面被系統回收(介面已經不存在),為了在下次開啟的時候恢復原來的樣子,系統為我們儲存介面的所有狀態,這個時候我們再去修改介面理論上肯定是不允許的,所以為了避免這種異常,要使用第二種方法。

3.懶載入

我們經常在使用fragment時,常常會結合著viewpager使用,那麼我們就會遇到一個問題,就是初始化fragment的時候,會連同我們寫的網路請求一起執行,這樣非常消耗效能,最理想的方式是,只有使用者點開或滑動到當前fragment時,才進行請求網路的操作。因此,我們就產生了懶載入這樣一個說法。

Viewpager配合fragment使用,預設載入前兩個fragment。很容易造成網路丟包、阻塞等問題。

在Fragment中有一個setUserVisibleHint這個方法,而且這個方法是優於onCreate()方法的,它會通過isVisibleToUser告訴我們當前Fragment我們是否可見,我們可以在可見的時候再進行網路載入。

從log上看setUserVisibleHint()的呼叫早於onCreateView,所以如果在setUserVisibleHint()要實現懶載入的話,就必須要確保View以及其他變數都已經初始化結束,避免空指標。

使用步驟:

申明一個變數isPrepare=false,isVisible=false,標明當前頁面是否被建立了 在onViewCreated週期內設定isPrepare=true 在setUserVisibleHint(boolean isVisible)判斷是否顯示,設定isVisible=true 判斷isPrepare和isVisible,都為true開始載入資料,然後恢復isPrepare和isVisible為false,防止重複載入。

4、Activity

4.1、 Activity啟動流程

使用者從Launcher程式點選應用圖示可啟動應用的入口Activity,Activity啟動時需要多個程序之間的互動,Android系統中有一個zygote程序專用於孵化Android框架層和應用層程式的程序。還有一個system_server程序,該程序裡執行了很多binder service。例如ActivityManagerService,PackageManagerService,WindowManagerService,這些binder service分別執行在不同的執行緒中,其中ActivityManagerService負責管理Activity棧,應用程序,task。

點選Launcher圖示來啟動Activity

使用者在Launcher程式裡點選應用圖示時,會通知ActivityManagerService啟動應用的入口Activity,ActivityManagerService發現這個應用還未啟動,則會通知Zygote程序孵化出應用程序,然後在這個dalvik應用程序裡執行ActivityThread的main方法。應用程序接下來通知ActivityManagerService應用程序已啟動,ActivityManagerService儲存應用程序的一個代理物件,這樣ActivityManagerService可以通過這個代理物件控制應用程序,然後ActivityManagerService通知應用程序建立入口Activity的範例,並執行它的生命週期方法。

4.2、Activity生命週期

(1)Activity的形態

Active/Running:

Activity處於活動狀態,此時Activity處於棧頂,是可見狀態,可與使用者進行互動。

Paused:

當Activity失去焦點時,或被一個新的非全螢幕的Activity,或被一個透明的Activity放置在棧頂時,Activity就轉化為Paused狀態。但我們需要明白,此時Activity只是失去了與使用者互動的能力,其所有的狀態資訊及其成員變數都還存在,只有在系統記憶體緊張的情況下,才有可能被系統回收掉。

Stopped:

當一個Activity被另一個Activity完全覆蓋時,被覆蓋的Activity就會進入Stopped狀態,此時它不再可見,但是跟Paused狀態一樣保持著其所有狀態資訊及其成員變數。

Killed:

當Activity被系統回收掉時,Activity就處於Killed狀態。

Activity會在以上四種形態中相互切換,至於如何切換,這因使用者的操作不同而異。瞭解了Activity的4種形態後,我們就來聊聊Activity的生命週期。

Activity的生命週期

所謂的典型的生命週期就是在有使用者參與的情況下,Activity經歷從建立,執行,停止,銷燬等正常的生命週期過程。

onCreate

該方法是在Activity被建立時回撥,它是生命週期第一個呼叫的方法,我們在建立Activity時一般都需要重寫該方法,然後在該方法中做一些初始化的操作,如通過setContentView設定介面佈局的資源,初始化所需要的元件資訊等。

onStart

此方法被回撥時表示Activity正在啟動,此時Activity已處於可見狀態,只是還沒有在前臺顯示,因此無法與使用者進行互動。可以簡單理解為Activity已顯示而我們無法看見擺了。

onResume

當此方法回撥時,則說明Activity已在前臺可見,可與使用者互動了(處於前面所說的Active/Running形態),onResume方法與onStart的相同點是兩者都表示Activity可見,只不過onStart回撥時Activity還是後臺無法與使用者互動,而onResume則已顯示在前臺,可與使用者互動。當然從流程圖,我們也可以看出當Activity停止後(onPause方法和onStop方法被呼叫),重新回到前臺時也會呼叫onResume方法,因此我們也可以在onResume方法中初始化一些資源,比如重新初始化在onPause或者onStop方法中釋放的資源。

onPause

此方法被回撥時則表示Activity正在停止(Paused形態),一般情況下onStop方法會緊接著被回撥。但通過流程圖我們還可以看到一種情況是onPause方法執行後直接執行了onResume方法,這屬於比較極端的現象了,這可能是使用者操作使當前Activity退居後臺後又迅速地再回到到當前的Activity,此時onResume方法就會被回撥。當然,在onPause方法中我們可以做一些資料儲存或者動畫停止或者資源回收的操作,但是不能太耗時,因為這可能會影響到新的Activity的顯示——onPause方法執行完成後,新Activity的onResume方法才會被執行。

onStop

一般在onPause方法執行完成直接執行,表示Activity即將停止或者完全被覆蓋(Stopped形態),此時Activity不可見,僅在後臺執行。同樣地,在onStop方法可以做一些資源釋放的操作(不能太耗時)。

onRestart

表示Activity正在重新啟動,當Activity由不可見變為可見狀態時,該方法被回撥。這種情況一般是使用者開啟了一個新的Activity時,當前的Activity就會被暫停(onPause和onStop被執行了),接著又回到當前Activity頁面時,onRestart方法就會被回撥。

onDestroy

此時Activity正在被銷燬,也是生命週期最後一個執行的方法,一般我們可以在此方法中做一些回收工作和最終的資源釋放。

小結

到這裡我們來個小結,當Activity啟動時,依次會呼叫onCreate(),onStart(),onResume(),而當Activity退居後臺時(不可見,點選Home或者被新的Activity完全覆蓋),onPause()和onStop()會依次被呼叫。當Activity重新回到前臺(從桌面回到原Activity或者被覆蓋後又回到原Activity)時,onRestart(),onStart(),onResume()會依次被呼叫。當Activity退出銷燬時(點選back鍵),onPause(),onStop(),onDestroy()會依次被呼叫,到此Activity的整個生命週期方法回撥完成。現在我們再回頭看看之前的流程圖,應該是相當清晰了吧。嗯,這就是Activity整個典型的生命週期過程。

2、 View部分知識點

Android的Activity、PhoneWindow和DecorView的關係可以用下面的圖表示:

2.1、DecorView淺析

例如,有下面一個檢視,DecorView為整個Window介面的最頂層View,它只有一個子元素LinearLayout。代表整個Window介面,包含通知欄、標題列、內容顯示欄三塊區域。其中LinearLayout中有兩個FrameLayout子元素。

DecorView的作用

DecorView是頂級View,本質是一個FrameLayout它包含兩部分,標題列和內容欄,都是FrameLayout。內容欄id是content,也就是activity中設定setContentView的部分,最終將佈局新增到id為content的FrameLayout中。 獲取content:ViewGroup content=findViewById(android.id.content) 獲取設定的View:getChildAt(0).

使用總結

每個Activity都包含一個Window物件,Window物件通常是由PhoneWindow實現的。 PhoneWindow:將DecorView設定為整個應用視窗的根View,是Window的實現類。它是Android中的最基本的視窗系統,每個Activity均會建立一個PhoneWindow物件,是Activity和整個View系統互動的介面。 DecorView:是頂層檢視,將要顯示的具體內容呈現在PhoneWindow上,DecorView是當前Activity所有View的祖先,它並不會向使用者呈現任何東西。

2.2、View的事件分發

View的事件分發機制可以使用下圖表示:

如上圖,圖分為3層,從上往下依次是Activity、ViewGroup、View。

  1. 事件從左上角那個白色箭頭開始,由Activity的dispatchTouchEvent做分發
  2. 箭頭的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是呼叫父類別實現。
  3. dispatchTouchEvent和 onTouchEvent的框裡有個【true---->消費】的字,表示的意思是如果方法返回true,那麼代表事件就此消費,不會繼續往別的地方傳了,事件終止。
  4. 目前所有的圖的事件是針對ACTION_DOWN的,對於ACTION_MOVE和ACTION_UP我們最後做分析。
  5. 之前圖中的Activity 的dispatchTouchEvent 有誤(圖已修復),只有return super.dispatchTouchEvent(ev) 才是往下走,返回true 或者 false 事件就被消費了(終止傳遞)。

ViewGroup事件分發

當一個點選事件產生後,它的傳遞過程將遵循如下順序:

Activity -> Window -> View

事件總是會傳遞給Activity,之後Activity再傳遞給Window,最後Window再傳遞給頂級的View,頂級的View在接收到事件後就會按照事件分發機制去分發事件。如果一個View的onTouchEvent返回了FALSE,那麼它的父容器的onTouchEvent將會被呼叫,依次類推,如果所有都不處理這個事件的話,那麼Activity將會處理這個事件。

對於ViewGroup的事件分發過程,大概是這樣的:如果頂級的ViewGroup攔截事件即onInterceptTouchEvent返回true的話,則事件會交給ViewGroup處理,如果ViewGroup的onTouchListener被設定的話,則onTouch將會被呼叫,否則的話onTouchEvent將會被呼叫,也就是說:兩者都設定的話,onTouch將會遮蔽掉onTouchEvent,在onTouchEvent中,如果設定了onClickerListener的話,那麼onClick將會被呼叫。如果頂級ViewGroup不攔截的話,那麼事件將會被傳遞給它所在的點選事件的子view,這時候子view的dispatchTouchEvent將會被呼叫

View的事件分發

dispatchTouchEvent -> onTouch(setOnTouchListener) -> onTouchEvent -> onClick

onTouch和onTouchEvent的區別 兩者都是在dispatchTouchEvent中呼叫的,onTouch優先於onTouchEvent,如果onTouch返回true,那麼onTouchEvent則不執行,及onClick也不執行。

2.3、View的繪製

在xml佈局檔案中,我們的layout_width和layout_height引數可以不用寫具體的尺寸,而是wrap_content或者是match_parent。這兩個設定並沒有指定真正的大小,可是我們繪製到螢幕上的View必須是要有具體的寬高的,正是因為這個原因,我們必須自己去處理和設定尺寸。當然了,View類給了預設的處理,但是如果View類的預設處理不滿足我們的要求,我們就得重寫onMeasure函數啦~。

onMeasure函數是一個int整數,裡面放了測量模式和尺寸大小。int型資料佔用32個bit,而google實現的是,將int資料的前面2個bit用於區分不同的佈局模式,後面30個bit存放的是尺寸的資料。 onMeasure函數的使用如下圖:

MeasureSpec有三種測量模式:image

match_parent—>EXACTLY。怎麼理解呢?match_parent就是要利用父View給我們提供的所有剩餘空間,而父View剩餘空間是確定的,也就是這個測量模式的整數裡面存放的尺寸。

wrap_content—>AT_MOST。怎麼理解:就是我們想要將大小設定為包裹我們的view內容,那麼尺寸大小就是父View給我們作為參考的尺寸,只要不超過這個尺寸就可以啦,具體尺寸就根據我們的需求去設定。

固定尺寸(如100dp)—>EXACTLY。使用者自己指定了尺寸大小,我們就不用再去幹涉了,當然是以指定的大小為主啦。

2.4、ViewGroup的繪製

自定義ViewGroup可就沒那麼簡單啦~,因為它不僅要管好自己的,還要兼顧它的子View。我們都知道ViewGroup是個View容器,它裝納child View並且負責把child View放入指定的位置。

  1. 首先,我們得知道各個子View的大小吧,只有先知道子View的大小,我們才知道當前的ViewGroup該設定為多大去容納它們。

  2. 根據子View的大小,以及我們的ViewGroup要實現的功能,決定出ViewGroup的大小

  3. ViewGroup和子View的大小算出來了之後,接下來就是去擺放了吧,具體怎麼去擺放呢?這得根據你客製化的需求去擺放了,比如,你想讓子View按照垂直順序一個挨著一個放,或者是按照先後順序一個疊一個去放,這是你自己決定的。

  4. 已經知道怎麼去擺放還不行啊,決定了怎麼擺放就是相當於把已有的空間」分割」成大大小小的空間,每個空間對應一個子View,我們接下來就是把子View對號入座了,把它們放進它們該放的地方去。

3、系統原理

3.1、打包原理

Android的包檔案APK分為兩個部分:程式碼和資源,所以打包方面也分為資源打包和程式碼打包兩個方面,這篇文章就來分析資源和程式碼的編譯打包原理。

具體說來:

  1. 通過AAPT工具進行資原始檔(包括AndroidManifest.xml、佈局檔案、各種xml資源等)的打包,生成R.java檔案。
  2. 通過AIDL工具處理AIDL檔案,生成相應的Java檔案。
  3. 通過Javac工具編譯專案原始碼,生成Class檔案。
  4. 通過DX工具將所有的Class檔案轉換成DEX檔案,該過程主要完成Java位元組碼轉換成Dalvik位元組碼,壓縮常數池以及清除冗餘資訊等工作。
  5. 通過ApkBuilder工具將資原始檔、DEX檔案打包生成APK檔案。
  6. 利用KeyStore對生成的APK檔案進行簽名。
  7. 如果是正式版的APK,還會利用ZipAlign工具進行對齊處理,對齊的過程就是將APK檔案中所有的資原始檔舉例檔案的起始距離都偏移4位元組的整數倍,這樣通過記憶體對映存取APK檔案的速度會更快。

3.2、安裝流程

Android apk的安裝過程主要氛圍以下幾步:

  1. 複製APK到/data/app目錄下,解壓並掃描安裝包。
  2. 資源管理器解析APK裡的資原始檔。
  3. 解析AndroidManifest檔案,並在/data/data/目錄下建立對應的應用資料目錄。
  4. 然後對dex檔案進行優化,並儲存在dalvik-cache目錄下。
  5. 將AndroidManifest檔案解析出的四大元件資訊註冊到PackageManagerService中。
  6. 安裝完成後,傳送廣播。

可以使用下面的圖表示:

4、 第三方庫解析

4.1、Retrofit網路請求框架

概念:Retrofit是一個基於RESTful的HTTP網路請求框架的封裝,其中網路請求的本質是由OKHttp完成的,而Retrofit僅僅負責網路請求介面的封裝。

原理:App應用程式通過Retrofit請求網路,實際上是使用Retrofit介面層封裝請求引數,Header、URL等資訊,之後由OKHttp完成後續的請求,在伺服器返回資料之後,OKHttp將原始的結果交給Retrofit,最後根據使用者的需求對結果進行解析。

retrofit使用

1.在retrofit中通過一個介面作為http請求的api介面

public interface NetApi {
    @GET("repos/{owner}/{repo}/contributors")
    Call<ResponseBody> contributorsBySimpleGetCall(@Path("owner") String owner, @Path("repo") String repo);
}

2.建立一個Retrofit範例

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .build();

3.呼叫api介面

NetApi repo = retrofit.create(NetApi.class);

//第三步:呼叫網路請求的介面獲取網路請求
retrofit2.Call<ResponseBody> call = repo.contributorsBySimpleGetCall("username", "path");
call.enqueue(new Callback<ResponseBody>() { //進行非同步請求
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        //進行非同步操作
    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        //執行錯誤回撥方法
    }
});

retrofit動態代理

retrofit執行的原理如下:

  1. 首先,通過method把它轉換成ServiceMethod。
  2. 然後,通過serviceMethod,args獲取到okHttpCall物件。
  3. 最後,再把okHttpCall進一步封裝並返回Call物件。 首先,建立retrofit物件的方法如下:
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .build();

在建立retrofit物件的時候用到了build()方法,該方法的實現如下:

public Retrofit build() {
  if (baseUrl == null) {
    throw new IllegalStateException("Base URL required.");
  }

  okhttp3.Call.Factory callFactory = this.callFactory;
  if (callFactory == null) {
    callFactory = new OkHttpClient(); //設定kHttpClient
  }

  Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {
    callbackExecutor = platform.defaultCallbackExecutor(); //設定預設回撥執行器
  }

  // Make a defensive copy of the adapters and add the default Call adapter.
  List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
  adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

  // Make a defensive copy of the converters.
  List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);

  return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
      callbackExecutor, validateEagerly); //返回新建的Retrofit物件
}

該方法返回了一個Retrofit物件,通過retrofit物件建立網路請求的介面的方式如下:

NetApi repo = retrofit.create(NetApi.class);

retrofit物件的create()方法的實現如下:‘

public <T> T create(final Class<T> service) {
  Utils.validateServiceInterface(service);
  if (validateEagerly) {
    eagerlyValidateMethods(service);
  }
  return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
      new InvocationHandler() {
        private final Platform platform = Platform.get();

        @Override public Object invoke(Object proxy, Method method, Object... args)
            throws Throwable {
          // If the method is a method from Object then defer to normal invocation.
          if (method.getDeclaringClass() == Object.class) {
            return method.invoke(this, args); //直接呼叫該方法
          }
          if (platform.isDefaultMethod(method)) {
            return platform.invokeDefaultMethod(method, service, proxy, args); //通過平臺物件呼叫該方法
          }
          ServiceMethod serviceMethod = loadServiceMethod(method); //獲取ServiceMethod物件
          OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); //傳入引數生成okHttpCall物件
          return serviceMethod.callAdapter.adapt(okHttpCall); //執行okHttpCall
        }
      });
}

4.2、圖片載入庫對比

Picasso:120K

Glide:475K

Fresco:3.4M

Android-Universal-Image-Loader:162K

圖片函數庫的選擇需要根據APP的具體情況而定,對於嚴重依賴圖片快取的APP,例如桌布類,圖片社交類APP來說,可以選擇最專業的Fresco。對於一般的APP,選擇Fresco會顯得比較重,畢竟Fresco3.4M的體量擺在這。根據APP對圖片的顯示和快取的需求從低到高,我們可以對以上函數庫做一個排序。

Picasso < Android-Universal-Image-Loader < Glide < Fresco

2.介紹:

Picasso :和Square的網路庫一起能發揮最大作用,因為Picasso可以選擇將網路請求的快取部分交給了okhttp實現。

Glide:模仿了Picasso的API,而且在他的基礎上加了很多的擴充套件(比如gif等支援),Glide預設的Bitmap格式是RGB_565,比 Picasso預設的ARGB_8888格式的記憶體開銷要小一半;Picasso快取的是全尺寸的(只快取一種),而Glide快取的是跟ImageView尺寸相同的(即5656和128128是兩個快取) 。

FB的圖片載入框架Fresco:最大的優勢在於5.0以下(最低2.3)的bitmap載入。在5.0以下系統,Fresco將圖片放到一個特別的記憶體區域(Ashmem區)。當然,在圖片不顯示的時候,佔用的記憶體會自動被釋放。這會使得APP更加流暢,減少因圖片記憶體佔用而引發的OOM。為什麼說是5.0以下,因為在5.0以後系統預設就是儲存在Ashmem區了。

3.總結:

Picasso所能實現的功能,Glide都能做,無非是所需的設定不同。但是Picasso體積比起Glide小太多如果專案中網路請求本身用的就是okhttp或者retrofit(本質還是okhttp),那麼建議用Picasso,體積會小很多(Square全家桶的幹活)。Glide的好處是大型的圖片流,比如gif、Video,如果你們是做美拍、愛拍這種視訊類應用,建議使用。

Fresco在5.0以下的記憶體優化非常好,代價就是體積也非常的大,按體積算Fresco>Glide>Picasso

不過在使用起來也有些不便(小建議:他只能用內建的一個ImageView來實現這些功能,用起來比較麻煩,我們通常是根據Fresco自己改改,直接使用他的Bitmap層)

4.3、各種json解析庫使用

(1)Google的Gson

Gson是目前功能最全的Json解析神器,Gson當初是為因應Google公司內部需求而由Google自行研發而來,但自從在2008年五月公開發布第一版後已被許多公司或使用者應用。Gson的應用主要為toJson與fromJson兩個轉換函數,無依賴,不需要例外額外的jar,能夠直接跑在JDK上。而在使用這種物件轉換之前需先建立好物件的型別以及其成員才能成功的將JSON字串成功轉換成相對應的物件。類裡面只要有get和set方法,Gson完全可以將複雜型別的json到bean或bean到json的轉換,是JSON解析的神器。Gson在功能上面無可挑剔,但是效能上面比FastJson有所差距。

(2)阿里巴巴的FastJson

Fastjson是一個Java語言編寫的高效能的JSON處理器,由阿里巴巴公司開發。

無依賴,不需要例外額外的jar,能夠直接跑在JDK上。FastJson在複雜型別的Bean轉換Json上會出現一些問題,可能會出現參照的型別,導致Json轉換出錯,需要制定參照。FastJson採用獨創的演演算法,將parse的速度提升到極致,超過所有json庫。

綜上Json技術的比較,在專案選型的時候可以使用Google的Gson和阿里巴巴的FastJson兩種並行使用,如果只是功能要求,沒有效能要求,可以使用google的Gson,如果有效能上面的要求可以使用Gson將bean轉換json確保資料的正確,使用FastJson將Json轉換Bean

5、熱點技術

5.1、元件化

(1)概念:

元件化:是將一個APP分成多個module,每個module都是一個元件,也可以是一個基礎庫供元件依賴,開發中可以單獨偵錯部分元件,元件中不需要相互依賴但是可以相互呼叫,最終釋出的時候所有元件以lib的形式被主APP工程依賴打包成一個apk。

(2)由來:

  1. APP版本迭代,新功能不斷增加,業務變得複雜,維護成本高
  2. 業務耦合度高,程式碼臃腫,團隊內部多人共同作業開發困難
  3. Android編譯程式碼卡頓,單一工程下程式碼耦合嚴重,修改一處需要重新編譯打包,耗時耗力。
  4. 方便單元測試,單獨改一個業務模組,不需要著重關注其他模組。

(3)優勢:

  1. 元件化將通用模組獨立出來,統一管理,以提高複用,將頁面拆分為粒度更小的元件,元件內部出了包含UI實現,還可以包含資料層和邏輯層
  2. 每個元件度可以獨立編譯、加快編譯速度、獨立打包。
  3. 每個工程內部的修改,不會影響其他工程。
  4. 業務庫工程可以快速拆分出來,整合到其他App中。
  5. 迭代頻繁的業務模組採用元件方式,業務線研發可以互不干擾、提升共同作業效率,並控制產品品質,加強穩定性。
  6. 並行開發,團隊成員只關注自己的開發的小模組,降低耦合性,後期維護方便等。

(4)考慮問題:

模式切換:如何使得APP在單獨偵錯跟整體偵錯自由切換

元件化後的每一個業務的module都可以是一個單獨的APP(isModuleRun=false), release 包的時候各個業務module作為lib依賴,這裡完全由一個變數控制,在根專案 gradle.properties裡面isModuleRun=true。isModuleRun狀態不同,載入application和AndroidManifest都不一樣,以此來區分是獨立的APK還是lib。

在build.grade裡面設定:

資源衝突

當我們建立了多個Module的時候,如何解決相同資原始檔名合併的衝突,業務Module和BaseModule資原始檔名稱重複會產生衝突,解決方案在於:

每個 module 都有 app_name,為了不讓資源名重名,在每個元件的 build.gradle 中增加 resourcePrefix 「xxx_強行檢查資源名稱字首。固定每個元件的資源字首。但是 resourcePrefix 這個值只能限定 xml 裡面的資源,並不能限定圖片資源。

依賴關係

多個Module之間如何參照一些共同的library以及工具類

元件通訊

元件化之後,Module之間是相互隔離的,如何進行UI跳轉以及方法呼叫,具體可以使用阿里巴巴ARouter或者美團的WMRouter等路由框架。

各業務Module之前不需要任何依賴可以通過路由跳轉,完美解決業務之間耦合。

入口引數

我們知道元件之間是有聯絡的,所以在單獨偵錯的時候如何拿到其它的Module傳遞過來的引數

Application

當元件單獨執行的時候,每個Module自成一個APK,那麼就意味著會有多個Application,很顯然我們不願意重複寫這麼多程式碼,所以我們只需要定義一個BaseApplication即可,其它的Application直接繼承此BaseApplication就OK了,BaseApplication裡面還可定義公用的引數。

5.2、外掛化

(1)概述

提到外掛化,就不得不提起方法數超過65535的問題,我們可以通過Dex分包來解決,同時也可以通過使用外掛化開發來解決。外掛化的概念就是由宿主APP去載入以及執行外掛APP。

(2優點)

在一個大的專案裡面,為了明確的分工,往往不同的團隊負責不同的外掛APP,這樣分工更加明確。各個模組封裝成不同的外掛APK,不同模組可以單獨編譯,提高了開發效率。 解決了上述的方法數超過限制的問題。可以通過上線新的外掛來解決線上的BUG,達到「熱修復」的效果。 減小了宿主APK的體積。

(3缺點)

外掛化開發的APP不能在Google Play上線,也就是沒有海外市場。

6、螢幕適配

6.1、基本概念

螢幕尺寸

含義:手機對角線的物理尺寸 單位:英寸(inch),1英寸=2.54cm

Android手機常見的尺寸有5寸、5.5寸、6寸,6.5寸等等

螢幕解析度

含義:手機在橫向、縱向上的畫素點數總和

一般描述成螢幕的」寬x高」=AxB 含義:螢幕在橫向方向(寬度)上有A個畫素點,在縱向方向

(高)有B個畫素點 例子:1080x1920,即寬度方向上有1080個畫素點,在高度方向上有1920個畫素點

單位:px(pixel),1px=1畫素點

UI設計師的設計圖會以px作為統一的計量單位

Android手機常見的解析度:320x480、480x800、720x1280、1080x1920

螢幕畫素密度

含義:每英寸的畫素點數 單位:dpi(dots per ich)

假設裝置內每英寸有160個畫素,那麼該裝置的螢幕畫素密度=160dpi

6.2、適配方法

1.支援各種螢幕尺寸: 使用wrap_content, match_parent, weight.要確保佈局的靈活性並適應各種尺寸的螢幕,應使用 「wrap_content」、「match_parent」 控制某些檢視元件的寬度和高度。

2.使用相對佈局,禁用絕對佈局。

3.使用LinearLayout的weight屬性

假如我們的寬度不是0dp(wrap_content和0dp的效果相同),則是match_parent呢?

android:layout_weight的真實含義是:如果View設定了該屬性並且有效,那麼該 View的寬度等於原有寬度(android:layout_width)加上剩餘空間的佔比。

從這個角度我們來解釋一下上面的現象。在上面的程式碼中,我們設定每個Button的寬度都是match_parent,假設螢幕寬度為L,那麼每個Button的寬度也應該都為L,剩餘寬度就等於L-(L+L)= -L。

Button1的weight=1,剩餘寬度佔比為1/(1+2)= 1/3,所以最終寬度為L+1/3*(-L)=2/3L,Button2的計算類似,最終寬度為L+2/3(-L)=1/3L。

4.使用.9圖片

6.3、今日頭條螢幕適配

7、效能優化

Android的效能優化,主要是從以下幾個方面進行優化的: 穩定(記憶體溢位、崩潰) 流暢(卡頓) 耗損(耗電、流量) 安裝包(APK瘦身) 影響穩定性的原因很多,比如記憶體使用不合理、程式碼異常場景考慮不周全、程式碼邏輯不合理等,都會對應用的穩定性造成影響。其中最常見的兩個場景是:Crash 和 ANR,這兩個錯誤將會使得程式無法使用。所以做好Crash全域性監控,處理閃退同時把崩潰資訊、異常資訊收集記錄起來,以便後續分析;合理使用主執行緒處理業務,不要在主執行緒中做耗時操作,防止ANR程式無響應發生。

(一)穩定——記憶體優化

(1)Memory Monitor 工具:

它是Android Studio自帶的一個記憶體監視工具,它可以很好地幫助我們進行記憶體實時分析。通過點選Android Studio右下角的Memory Monitor標籤,開啟工具可以看見較淺藍色代表free的記憶體,而深色的部分代表使用的記憶體從記憶體變換的走勢圖變換,可以判斷關於記憶體的使用狀態,例如當記憶體持續增高時,可能發生記憶體漏失;當記憶體突然減少時,可能發生GC等,如下圖所示。

LeakCanary工具: LeakCanary是Square公司基於MAT開發的一款監控Android記憶體漏失的開源框架。其工作的原理是: 監測機制利用了Java的WeakReference和ReferenceQueue,通過將Activity包裝到WeakReference中,被WeakReference包裝過的Activity物件如果被回收,該WeakReference參照會被放到ReferenceQueue中,通過監測ReferenceQueue裡面的內容就能檢查到Activity是否能夠被回收(在ReferenceQueue中說明可以被回收,不存在洩漏;否則,可能存在洩漏,LeakCanary是執行一遍GC,若還未在ReferenceQueue中,就會認定為洩漏)。

如果Activity被認定為洩露了,就抓取記憶體dump檔案(Debug.dumpHprofData);之後通過HeapAnalyzerService.runAnalysis進行分析記憶體檔案分析;接著通過HeapAnalyzer (checkForLeak—findLeakingReference—findLeakTrace)來進行記憶體漏失分析。最後通過DisplayLeakService進行記憶體漏失的展示。

(3)Android Lint 工具:

Android Lint Tool 是Android Sutido種整合的一個Android程式碼提示工具,它可以給你佈局、程式碼提供非常強大的幫助。寫死會提示以級別警告,例如:在佈局檔案中寫了三層冗餘的LinearLayout佈局、直接在TextView中寫要顯示的文字、字型大小使用dp而不是sp為單位,就會在編輯器右邊看到提示。

(二)流暢——卡頓優化

卡頓的場景通常是發生在使用者互動體驗最直接的方面。影響卡頓的兩大因素,分別是介面繪製和資料處理。

介面繪製:主要原因是繪製的層級深、頁面複雜、重新整理不合理,由於這些原因導致卡頓的場景更多出現在 UI 和啟動後的初始介面以及跳轉到頁面的繪製上。

資料處理:導致這種卡頓場景的原因是資料處理量太大,一般分為三種情況,一是資料在處理 UI 執行緒,二是資料處理佔用 CPU 高,導致主執行緒拿不到時間片,三是記憶體增加導致 GC 頻繁,從而引起卡頓。

(1)佈局優化

在Android種系統對View進行測量、佈局和繪製時,都是通過對View數的遍歷來進行操作的。如果一個View數的高度太高就會嚴重影響測量、佈局和繪製的速度。Google也在其API檔案中建議View高度不宜哦過10層。現在版本種Google使用RelativeLayout替代LineraLayout作為預設根佈局,目的就是降低LineraLayout巢狀產生布局樹的高度,從而提高UI渲染的效率。

佈局複用,使用標籤重用layout; 提高顯示速度,使用延遲View載入; 減少層級,使用標籤替換父級佈局; 注意使用wrap_content,會增加measure計算成本; 刪除控制元件中無用屬性;

(2)繪製優化

過度繪製是指在螢幕上的某個畫素在同一幀的時間內被繪製了多次。在多層次重疊的 UI 結構中,如果不可見的 UI 也在做繪製的操作,就會導致某些畫素區域被繪製了多次,從而浪費了多餘的 CPU 以及 GPU 資源。如何避免過度繪製?

佈局上的優化。移除 XML 中非必須的背景,移除 Window 預設的背景、按需顯示佔位背景圖片

自定義View優化。使用 canvas.clipRect() 幫助系統識別那些可見的區域,只有在這個區域內才會被繪製。

(3)啟動優化

應用一般都有閃屏頁SplashActivity,優化閃屏頁的 UI 佈局,可以通過 Profile GPU Rendering 檢測丟幀情況。

(三)節省——耗電優化

在 Android5.0 以前,關於應用電量消耗的測試即麻煩又不準確,而5.0 之後Google專門引入了一個獲取裝置上電量消耗資訊的API—— Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系統電量分析工具,直觀地展示出手機的電量消耗過程,通過輸入電量分析檔案,顯示消耗情況。

最後提供一些可供參考耗電優化的方法:

(1)計算優化。演演算法、for迴圈優化、Switch…case替代if…else、避開浮點運算。

浮點運算:計算機裡整數和小數形式就是按普通格式進行儲存,例如1024、3.1415926等等,這個沒什麼特點,但是這樣的數精度不高,表達也不夠全面,為了能夠有一種數的通用表示法,就發明了浮點數。浮點數的表示形式有點像科學計數法(.×10),它的表示形式是0.×10,在計算機中的形式為 .* e ±**),其中前面的星號代表定點小數,也就是整數部分為0的純小數,後面的指數部分是定點整數。利用這樣的形式就能表示出任意一個整數和小數,例如1024就能表示成0.1024×10^4,也就是 .1024e+004,3.1415926就能表示成0.31415926×10^1,也就是 .31415926e+001,這就是浮點數。浮點數進行的運算就是浮點運算。浮點運算比常規運算更復雜,因此計算機進行浮點運算速度要比進行常規運算慢得多。

(2)避免 Wake Lock 使用不當。

Wake Lock是一種鎖的機制,主要是相對系統的休眠而言的,,只要有人拿著這個鎖,系統就無法進入休眠意思就是我的程式給CPU加了這個鎖那系統就不會休眠了,這樣做的目的是為了全力配合我們程式的執行。有的情況如果不這麼做就會出現一些問題,比如微信等及時通訊的心跳包會在熄屏不久後停止網路存取等問題。所以微信裡面是有大量使用到了Wake_Lock鎖。系統為了節省電量,CPU在沒有任務忙的時候就會自動進入休眠。有任務需要喚醒CPU高效執行的時候,就會給CPU加Wake_Lock鎖。大家經常犯的錯誤,我們很容易去喚醒CPU來工作,但是很容易忘記釋放Wake_Lock。

(3)使用 Job Scheduler 管理後臺任務。

在Android 5.0 API 21 中,google提供了一個叫做JobScheduler API的元件,來處理當某個時間點或者當滿足某個特定的條件時執行一個任務的場景,例如當使用者在夜間休息時或裝置接通電源介面卡連線WiFi啟動下載更新的任務。這樣可以在減少資源消耗的同時提升應用的效率。

(四)安裝包——APK瘦身

(1)安裝包的組成結構

assets資料夾。存放一些組態檔、資原始檔,assets不會自動生成對應的 ID,而是通過 AssetManager 類的介面獲取。

res。res 是 resource 的縮寫,這個目錄存放資原始檔,會自動生成對應的 ID 並對映到 .R 檔案中,存取直接使用資源 ID。

META-INF。儲存應用的簽名資訊,簽名資訊可以驗證 APK 檔案的完整性。

AndroidManifest.xml。這個檔案用來描述 Android 應用的設定資訊,一些元件的註冊資訊、可使用許可權等。

classes.dex。Dalvik 位元組碼程式,讓 Dalvik 虛擬機器器可執行,一般情況下,Android 應用在打包時通過 Android SDK 中的 dx 工具將 Java 位元組碼轉換為 Dalvik 位元組碼。

resources.arsc。記錄著資原始檔和資源 ID 之間的對映關係,用來根據資源 ID 尋找資源。

(2)減少安裝包大小

程式碼混淆。使用IDE 自帶的 proGuard 程式碼混淆器工具 ,它包括壓縮、優化、混淆等功能。 資源優化。比如使用 Android Lint 刪除冗餘資源,資原始檔最少化等。 圖片優化。比如利用 PNG優化工具 對圖片做壓縮處理。推薦目前最先進的壓縮工具Googlek開源庫zopfli。如果應用在0版本以上,推薦使用 WebP圖片格式。 避免重複或無用功能的第三方庫。例如,百度地圖接入基礎地圖即可、訊飛語音無需接入離線、圖片庫Glide\Picasso等。 外掛化開發。比如功能模組放在伺服器上,按需下載,可以減少安裝包大小。 可以使用微信開源資原始檔混淆工具——AndResGuard。一般可以壓縮apk的1M左右大。

7.1、冷啟動與熱啟動

冷啟動 在啟動應用時,系統中沒有該應用的程序,這時系統會建立一個新的程序分配給該應用;

熱啟動 在啟動應用時,系統中已有該應用的程序(例:按back鍵、home鍵,應用雖然會退出,但是該應用的程序還是保留在後臺);

區別 冷啟動:系統沒有該應用的程序,需要建立一個新的程序分配給應用,所以會先建立和初始化Application類,再建立和初始化MainActivity類(包括一系列的測量、佈局、繪製),最後顯示在介面上。 熱啟動: 從已有的程序中來啟動,不會建立和初始化Application類,直接建立和初始化MainActivity類(包括一系列的測量、佈局、繪製),最後顯示在介面上。

冷啟動流程 Zygote程序中fork建立出一個新的程序; 建立和初始化Application類、建立MainActivity; inflate佈局、當onCreate/onStart/onResume方法都走完; contentView的measure/layout/draw顯示在介面上。

冷啟動優化 減少在Application和第一個Activity的onCreate()方法的工作量; 不要讓Application參與業務的操作; 不要在Application進行耗時操作; 不要以靜態變數的方式在Application中儲存資料; 減少佈局的複雜性和深度;

8、MVP模式架構

8.1、MVP模式

MVP架構由MVC發展而來。在MVP中,M代表Model,V代表View,P代表Presenter。

模型層(Model):主要是獲取資料功能,業務邏輯和實體模型。

檢視層(View):對應於Activity或Fragment,負責檢視的部分展示和業務邏輯使用者互動

控制層(Presenter):負責完成View層與Model層間的互動,通過P層來獲取M層中資料後返回給V層,使得V層與M層間沒有耦合。

在MVP中 ,Presenter層完全將View層和Model層進行了分離,把主要程式邏輯放在Presenter層實現,Presenter與具體的View層(Activity)是沒有直接的關聯,是通過定義介面來進行互動的,從而使得當View層(Activity)發生改變時,Persenter依然可以保持不變。View層介面類只應該只有set/get方法,及一些介面顯示內容和使用者輸入,除此之外不應該有多餘的內容。絕不允許View層直接存取Model層,這是與MVC最大區別之處,也是MVP核心優點。

9、虛擬機器器

9.1、Android Dalvik虛擬機器器和ART虛擬機器器對比

Dalvik

Android4.4及以前使用的都是Dalvik虛擬機器器,我們知道Apk在打包的過程中會先將java等原始碼通過javac編譯成.class檔案,但是我們的Dalvik虛擬機器器只會執行.dex檔案,這個時候dx會將.class檔案轉換成Dalvik虛擬機器器執行的.dex檔案。Dalvik虛擬機器器在啟動的時候會先將.dex檔案轉換成快速執行的機器碼,又因為65535這個問題,導致我們在應用冷啟動的時候有一個合包的過程,最後導致的一個結果就是我們的app啟動慢,這就是Dalvik虛擬機器器的JIT特性(Just In Time)。

ART

ART虛擬機器器是在Android5.0才開始使用的Android虛擬機器器,ART虛擬機器器必須要相容Dalvik虛擬機器器的特性,但是ART有一個很好的特性AOT(ahead of time),這個特性就是我們在安裝APK的時候就將dex直接處理成可直接供ART虛擬機器器使用的機器碼,ART虛擬機器器將.dex檔案轉換成可直接執行的.oat檔案,ART虛擬機器器天生支援多dex,所以也不會有一個合包的過程,所以ART虛擬機器器會很大的提升APP冷啟動速度。

ART優點:

加快APP冷啟動速度

提升GC速度

提供功能全面的Debug特性

ART缺點:

APP安裝速度慢,因為在APK安裝的時候要生成可執行.oat檔案

APK佔用空間大,因為在APK安裝的時候要生成可執行.oat檔案

arm處理器

總結

熟悉Android效能分析工具、UI卡頓、APP啟動、包瘦身和記憶體效能優化

熟悉Android APP架構設計,模組化、元件化、外掛化開發

熟練掌握Java、設計模式、網路、多執行緒技術

Java基本知識點

1、Java的類載入過程

jvm將.class類檔案資訊載入到記憶體並解析成對應的class物件的過程,注意:jvm並不是一開始就把所有的類載入進記憶體中,只是在第一次遇到某個需要執行的類才會載入,並且只載入一次

主要分為三部分:1、載入,2、連結(1.驗證,2.準備,3.解析),3、初始化

1:載入

類載入器包括 BootClassLoader、ExtClassLoader、APPClassLoader

2:連結

驗證:(驗證class檔案的位元組流是否符合jvm規範)

準備:為類變數分配記憶體,並且進行賦初值

解析:將常數池裡面的符號參照(變數名)替換成直接參照(記憶體地址)過程,在解析階段,jvm會把所有的類名、方法名、欄位名、這些符號參照替換成具體的記憶體地址或者偏移量。

3:初始化

主要對類變數進行初始化,執行類構造器的過程,換句話說,只對static修試的變數或者語句進行初始化。

範例:Person person = new Person();為例進行說明。

Java程式設計思想中的類的初始化過程主要有以下幾點:

  1. 找到class檔案,將它載入到記憶體
  2. 在堆記憶體中分配記憶體地址
  3. 初始化
  4. 將堆記憶體地址指給棧記憶體中的p變數

2、String、StringBuilder、StringBuffer

StringBuffer裡面的很多方法新增了synchronized關鍵字,是可以表徵執行緒安全的,所以多執行緒情況下使用它。

執行速度:

StringBuilder > StringBuffer > String

StringBuilder犧牲了效能來換取速度的,這兩個是可以直接在原物件上面進行修改,省去了建立新物件和回收老物件的過程,而String是字串常數(final)修試,另外兩個是字串變數,常數物件一旦建立就不可以修改,變數是可以進行修改的,所以對於String字串的操作包含下面三個步驟:

  1. 建立一個新物件,名字和原來的一樣
  2. 在新物件上面進行修改
  3. 原物件被垃圾回收掉

3、JVM記憶體結構

Java物件範例化過程中,主要使用到虛擬機器器棧、Java堆和方法區。Java檔案經過編譯之後首先會被載入到jvm方法區中,jvm方法區中很重的一個部分是執行時常數池,用以儲存class檔案類的版本、欄位、方法、介面等描述資訊和編譯期間的常數和靜態常數。

3.1、JVM基本結構

類載入器classLoader,在JVM啟動時或者類執行時將需要的.class檔案載入到記憶體中。 執行引擎,負責執行class檔案中包含的位元組碼指令。 本地方法介面,主要是呼叫C/C++實現的本地方法及返回結果。 記憶體區域(執行時資料區),是在JVM執行的時候操作所分配的記憶體區, 主要分為以下五個部分,如下圖:

  • 方法區:用於儲存類結構資訊的地方,包括常數池、靜態變數、建構函式等。
  • Java堆(heap):儲存Java範例或者物件的地方。這塊是gc的主要區域。
  • Java棧(stack):Java棧總是和執行緒關聯的,每當建立一個執行緒時,JVM就會為這個執行緒建立一個對應的Java棧。在這個java棧中又會包含多個棧幀,每執行一個方法就建立一個棧幀,用於儲存區域性變數表、操作棧、方法返回值等。每一個方法從呼叫直至執行完成的過程,就對應一個棧幀在java棧中入棧到出棧的過程。所以java棧是執行緒私有的。
  • 程式計數器:用於儲存當前執行緒執行的記憶體地址,由於JVM是多執行緒執行的,所以為了保證執行緒切換回來後還能恢復到原先狀態,就需要一個獨立的計數器,記錄之前中斷的地方,可見程式計數器也是執行緒私有的。
  • 本地方法棧:和Java棧的作用差不多,只不過是為JVM使用到的native方法服務的。

3.2、JVM原始碼分析

4、GC機制

垃圾收集器一般完成兩件事

  1. 檢測出垃圾;
  2. 回收垃圾;

4.1 Java物件參照

通常,Java物件的參照可以分為4類:強參照、軟參照、弱參照和虛參照。 強參照:通常可以認為是通過new出來的物件,即使記憶體不足,GC進行垃圾收集的時候也不會主動回收。

Object obj = new Object();

軟參照:在記憶體不足的時候,GC進行垃圾收集的時候會被GC回收。

Object obj = new Object();
SoftReference<Object> softReference = new SoftReference<>(obj);

弱參照:無論記憶體是否充足,GC進行垃圾收集的時候都會回收。

Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);

虛參照:和弱參照類似,主要區別在於虛參照必須和參照佇列一起使用。

Object obj = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);

參照佇列:如果軟參照和弱參照被GC回收,JVM就會把這個參照加到參照佇列裡,如果是虛參照,在回收前就會被加到參照佇列裡。

垃圾檢測方法:

參照計數法:給每個物件新增參照計數器,每個地方參照它,計數器就+1,失效時-1。如果兩個物件互相參照時,就導致無法回收。 可達性分析演演算法:以根集物件為起始點進行搜尋,如果物件不可達的話就是垃圾物件。根集(Java棧中參照的物件、方法區中常數池中參照的物件、本地方法中參照的物件等。JVM在垃圾回收的時候,會檢查堆中所有物件是否被這些根集物件參照,不能夠被參照的物件就會被垃圾回收器回收。)

垃圾回收演演算法:

常見的垃圾回收演演算法有:

標記-清除

標記:首先標記所有需要回收的物件,在標記完成之後統計回收所有被標記的物件,它的標記過程即為上面的可達性分析演演算法。 清除:清除所有被標記的物件 缺點: 效率不足,標記和清除效率都不高 空間問題,標記清除之後會產生大量不連續的記憶體碎片,導致大物件分配無法找到足夠的空間,提前進行垃圾回收。

複製回收演演算法 將可用的記憶體按容量劃分為大小相等的2塊,每次只用一塊,當這一塊的記憶體用完了,就將存活的物件複製到另外一塊上面,然後把已使用過的記憶體空間一次清理掉。

缺點:

將記憶體縮小了原本的一般,代價比較高 大部分物件是「朝生夕滅」的,所以不必按照1:1的比例劃分。 現在商業虛擬機器器採用這種演演算法回收新生代,但不是按1:1的比例,而是將記憶體區域劃分為eden 空間、from 空間、to 空間 3 個部分。 其中 from 空間和 to 空間可以視為用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱為 survivor 空間,即倖存者空間,用於存放未被回收的物件。

在垃圾回收時,eden 空間中的存活物件會被複制到未使用的 survivor 空間中 (假設是 to),正在使用的 survivor 空間 (假設是 from) 中的年輕物件也會被複制到 to 空間中 (大物件,或者老年物件會直接進入老年帶,如果 to 空間已滿,則物件也會直接進入老年代)。此時,eden 空間和 from 空間中的剩餘物件就是垃圾物件,可以直接清空,to 空間則存放此次回收後的存活物件。這種改進的複製演演算法既保證了空間的連續性,又避免了大量的記憶體空間浪費。

標記-整理

在老年代的物件大都是存活物件,複製演演算法在物件存活率教高的時候,效率就會變得比較低。根據老年代的特點,有人提出了「標記-壓縮演演算法(Mark-Compact)」

標記過程與標記-清除的標記一樣,但後續不是對可回收物件進行清理,而是讓所有的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

這種方法既避免了碎片的產生,又不需要兩塊相同的記憶體空間,因此,其價效比比較高。

分帶收集演演算法

根據物件存活的週期不同將記憶體劃分為幾塊,一般是把Java堆分為老年代和新生代,這樣根據各個年代的特點採用適當的收集演演算法。

新生代每次收集都有大量物件死去,只有少量存活,那就選用複製演演算法,複製的物件數較少就可完成收集。 老年代物件存活率高,使用標記-壓縮演演算法,以提高垃圾回收效率。

5、類載入器

程式在啟動的時候,並不會一次性載入程式所要用的所有class檔案,而是根據程式的需要,通過Java的類載入機制(ClassLoader)來動態載入某個class檔案到記憶體當中的,從而只有class檔案被載入到了記憶體之後,才能被其它class所參照。所以ClassLoader就是用來動態載入class檔案到記憶體當中用的。

5.1、雙親委派原理

每個ClassLoader範例都有一個父類別載入器的參照(不是繼承關係,是一個包含的關係),虛擬機器器內建的類載入器(Bootstrap ClassLoader)本身沒有父類別載入器,但是可以用做其他ClassLoader範例的父類別載入器。

當一個ClassLoader 範例需要載入某個類時,它會試圖在親自搜尋這個類之前先把這個任務委託給它的父類別載入器,這個過程是由上而下依次檢查的,首先由頂層的類載入器Bootstrap CLassLoader進行載入,如果沒有載入到,則把任務轉交給Extension CLassLoader檢視載入,如果也沒有找到,則轉交給AppCLassLoader進行載入,還是沒有的話,則交給委託的發起者,由它到指定的檔案系統或者網路等URL中進行載入類。還沒有找到的話,則會丟擲CLassNotFoundException異常。否則將這個類生成一個類的定義,並將它載入到記憶體中,最後返回這個類在記憶體中的Class範例物件。

5.2、 為什麼使用雙親委託模型

JVM在判斷兩個class是否相同時,不僅要判斷兩個類名是否相同,還要判斷是否是同一個類載入器載入的。

避免重複載入,父類別已經載入了,則子CLassLoader沒有必要再次載入。 考慮安全因素,假設自定義一個String類,除非改變JDK中CLassLoader的搜尋類的預設演演算法,否則使用者自定義的CLassLoader如法載入一個自己寫的String類,因為String類在啟動時就被引導類載入器Bootstrap CLassLoader載入了。

6、集合

Java集合類主要由兩個介面派生出:Collection和Map,這兩個介面是Java集合的根介面。

Collection介面是集合類的根介面,Java中沒有提供這個介面的直接的實現類。但是卻讓其被繼承產生了兩個介面,就是 Set和List。Set中不能包含重複的元素。List是一個有序的集合,可以包含重複的元素,提供了按索引存取的方式。

Map是Java.util包中的另一個介面,它和Collection介面沒有關係,是相互獨立的,但是都屬於集合類的一部分。Map包含了key-value對。Map不能包含重複的key,但是可以包含相同的value。

6.1、區別

List,Set都是繼承自Collection介面,Map則不是; List特點:元素有放入順序,元素可重複; Set特點:元素無放入順序,元素不可重複,重複元素會覆蓋掉,(注意:元素雖然無放入順序,但是元素在set中的位置是有該元素的HashCode決定的,其位置其實是固定的,加入Set 的Object必須定義equals()方法; LinkedList、ArrayList、HashSet是非執行緒安全的,Vector是執行緒安全的; HashMap是非執行緒安全的,HashTable是執行緒安全的;

6.2、List和Vector比較

Vector是多執行緒安全的,執行緒安全就是說多執行緒存取同一程式碼,不會產生不確定的結果。而ArrayList不是,這個可以從原始碼中看出,Vector類中的方法很多有synchronized進行修飾,這樣就導致了Vector在效率上無法與ArrayList相比; 兩個都是採用的線性連續空間儲存元素,但是當空間不足的時候,兩個類的增加方式是不同。 Vector可以設定增長因子,而ArrayList不可以。 Vector是一種老的動態陣列,是執行緒同步的,效率很低,一般不贊成使用。

6.3、HashSet如何保證不重複

HashSet底層通過HashMap來實現的,在往HashSet中新增元素是

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

在HashMap中進行查詢是否存在這個key,value始終是一樣的,主要有以下幾種情況:

  • 如果hash碼值不相同,說明是一個新元素,存;
  • 如果hash碼值相同,且equles判斷相等,說明元素已經存在,不存;
  • 如果hash碼值相同,且equles判斷不相等,說明元素不存在,存;
  • 如果有元素和傳入物件的hash值相等,那麼,繼續進行equles()判斷,如果仍然相等,那麼就認為傳入元素已經存在,不再新增,結束,否則仍然新增;

6.4、HashSet與Treeset的適用場景

  • HashSet是基於Hash演演算法實現的,其效能通常都優於TreeSet。為快速查詢而設計的Set,我們通常都應該使用HashSet,在我們需要排序的功能時,我們才使用TreeSet。
  • TreeSet 是二元樹(紅黑樹的樹據結構)實現的,Treeset中的資料是自動排好序的,不允許放入null值
  • HashSet是雜湊表實現的,HashSet中的資料是無序的,可以放入null,但只能放入一個null,兩者中的值都不能重複,就如資料庫中唯一約束。
  • HashSet是基於Hash演演算法實現的,其效能通常都優於TreeSet。為快速查詢而設計的Set,我們通常都應該使用HashSet,在我們需要排序的功能時,我們才使用TreeSet。

6.5、HashMap與TreeMap、HashTable的區別及適用場景

HashMap 非執行緒安全,基於雜湊表(雜湊表)實現。使用HashMap要求新增的鍵類明確定義了hashCode()和equals()[可以重寫hashCode()和equals()],為了優化HashMap空間的使用,您可以調優初始容量和負載因子。其中雜湊表的衝突處理主要分兩種,一種是開放定址法,另一種是連結串列法。HashMap的實現中採用的是連結串列法。 TreeMap:非執行緒安全基於紅黑樹實現,TreeMap沒有調優選項,因為該樹總處於平衡狀態

7、 常數池

7.1、Interger中的128(-128~127)

當數值範圍為-128~127時:如果兩個new出來Integer物件,即使值相同,通過「」比較結果為false,但兩個物件直接賦值,則通過「」比較結果為「true,這一點與String非常相似。 當數值不在-128~127時,無論通過哪種方式,即使兩個物件的值相等,通過「」比較,其結果為false; 當一個Integer物件直接與一個int基本資料型別通過「」比較,其結果與第一點相同; Integer物件的hash值為數值本身;

@Override
public int hashCode() {
return Integer.hashCode(value);
}

7.2、為什麼是-128-127?

在Integer類中有一個靜態內部類IntegerCache,在IntegerCache類中有一個Integer陣列,用以快取當數值範圍為-128~127時的Integer物件。

8、泛型

泛型是Java SE 1.5的新特性,泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。 Java語言引入泛型的好處是安全簡單。

泛型的好處是在編譯的時候檢查型別安全,並且所有的強制轉換都是自動和隱式的,提高程式碼的重用率。

它提供了編譯期的型別安全,確保你只能把正確型別的物件放入 集合中,避免了在執行時出現ClassCastException。

使用Java的泛型時應注意以下幾點:

  • 泛型的型別引數只能是類型別(包括自定義類),不能是簡單型別。
  • 同一種泛型可以對應多個版本(因為引數型別是不確定的),不同版本的泛型類範例是不相容的。
  • 泛型的型別引數可以有多個。
  • 泛型的引數型別可以使用extends語句,例如。習慣上稱為「有界型別」。
  • 泛型的引數型別還可以是萬用字元型別。例如Class<?> classType = Class.forName(「java.lang.String」);

8.1 T泛型和萬用字元泛型

  • ? 表示不確定的java型別。
  • T 表示java型別。
  • K V 分別代表java鍵值中的Key Value。
  • E 代表Element。

8.2 泛型擦除

Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java位元組碼中是不包含泛型中的型別資訊的。使用泛型的時候加上的型別引數,會在編譯器在編譯的時候去掉。這個過程就稱為型別擦除。

泛型是通過型別擦除來實現的,編譯器在編譯時擦除了所有型別相關的資訊,所以在執行時不存在任何型別相關的資訊。例如 List在執行時僅用一個List來表示。這樣做的目的,是確保能和Java 5之前的版本開發二進位制類庫進行相容。你無法在執行時存取到型別引數,因為編譯器已經把泛型型別轉換成了原始型別。

8.3 限定萬用字元

一種是<? extends T>它通過確保型別必須是T的子類來設定型別的上界, 另一種是<? super T>它通過確保型別必須是T的父類別來設定型別的下界。 另一方面表 示了非限定萬用字元,因為可以用任意型別來替代。 例如List<? extends Number>可以接受List或List。

8.4 泛型面試題

你可以把List傳遞給一個接受List引數的方法嗎?

對任何一個不太熟悉泛型的人來說,這個Java泛型題目看起來令人疑惑,因為乍看起來String是一種Object,所以 List應當可以用在需要List的地方,但是事實並非如此。真這樣做的話會導致編譯錯誤。如 果你再深一步考慮,你會發現Java這樣做是有意義的,因為List可以儲存任何型別的物件包括String, Integer等等,而List卻只能用來儲存Strings。

Array中可以用泛型嗎?

Array事實上並不支援泛型,這也是為什麼Joshua Bloch在Effective Java一書中建議使用List來代替Array,因為List可以提供編譯期的型別安全保證,而Array卻不能。

9、反射

9.1、概念

JAVA反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為java語言的反射機制。

9.2、作用

Java反射機制主要提供了以下功能: 在執行時判斷任意一個物件所屬的類;在執行時構造任意一個類的物件;在執行時判斷任意一個類所具有的成員變數和方法;在執行時呼叫任意一個物件的方法;生成動態代理。

資料結構與演演算法

1、排序

排序有內部排序和外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是因排序的資料很大,一次不能容納全部的排序記錄,在排序過程中需要存取外存。

1.1、 直接插入排序

思想:

將第一個數和第二個數排序,然後構成一個有序序列 將第三個數插入進去,構成一個新的有序序列。 對第四個數、第五個數……直到最後一個數,重複第二步。 程式碼:

首先設定插入次數,即迴圈次數,for(int i=1;i<length;i++),1個數的那次不用插入。 設定插入數和得到已經排好序列的最後一個數的位數。insertNum和j=i-1。

2、設計模式

2.1、單例設計模式

單例主要分為:懶漢式單例、餓漢式單例、登記式單例。

特點:

  1. 單例類只有一個範例
  2. 單例類必須自己建立自己的唯一範例
  3. 單例類必須給所有其他物件提供這一範例。

在計算機系統中,像執行緒池,快取、紀錄檔物件、對話方塊、印表機等常被設計成單例。

懶漢式單例:

Singleton通過將構造方法限定為private避免了類在外部被範例化,在同一個虛擬機器器範圍內,Singleton的唯一範例只能通過getInstance()方法存取。(事實上,通過Java反射機制是能夠範例化構造方法為private的類的,那基本上會使所有的Java單例實現失效。

它是執行緒不安全的,並行情況下很有可能出現多個Singleton範例,要實現執行緒安全,有以下三種方式: 1.在getInstance方法上加上同步

2.雙重檢查鎖定

3.靜態內部類

這種方式對比前兩種,既實現了執行緒安全,又避免了同步帶來的效能影響。

餓漢式單例:

餓漢式在建立類的同時就已經建立好了一個靜態的物件供系統使用,以後不再改變,所以天生是系統安全。

最後

漫漫開發之路,你我只是其中的一小部分……只有不斷的學習、進階,才是我們的出路!才跟得上時代的進步!

如果你看到了這裡,覺得文章寫得不錯就給個唄?如果你覺得那裡值得改進的,請給我留言。一定會認真查詢,修正不足。謝謝。

希望讀到這的您能轉發分享關注一下我,以後還會更新技術乾貨,謝謝您的支援!

有一句老話說的好:**「比你優秀的對手在學習,你的仇人在磨刀,你的閨蜜在減肥,隔壁老王在練腰,我們必須不斷學習,否則我們將被學習者超越。」**當然一個人學習是枯燥的,還需要一個良好的學習氛圍,因此我組建了一個學習交流探討的社群,歡迎大家一起來交流探討共同進步。還有一些收集整理的資料,感興趣的可以加群,一起學習,共同進步!

這邊把我收錄整理的一些資料拿出來分享給大家,一方面是希望能夠幫助大家提高,一方面也是警醒自己,要不斷學習、不斷提升,進階才是王道!

分享給大家的資料包括 高階架構技術進階腦圖Android開發面試專題資料,還有 高階進階架構資料包括但不限於 【高階UI、效能優化、移動架構師、NDK、混合式開發(ReactNative+Weex)微信小程式、Flutter等全方面的Android進階實踐技術】希望能幫助大家學習提升進階,也節省大家在網上搜尋資料的時間來學習,也是可以分享給身邊好友一起學習的!希望能幫助大家學習提升進階,也節省大家在網上搜尋資料的時間來學習,也是可以分享給身邊好友一起學習的!

Android架構師之路很漫長,一起共勉吧!

面試難免讓人焦慮不安。經歷過的人都懂的。但是如果你提前預測面試官要問你的問題並想出得體的回答方式,就會容易很多。

上述面試題答案都整理成檔案筆記。 也還整理了一些Android學習PDF+架構視訊+原始碼筆記高階架構技術進階腦圖、Android開發面試專題資料,高階進階架構資料

這些都是我現在閒暇還會反覆翻閱的精品資料。裡面對近幾年的大廠面試高頻知識點都有詳細的講解。相信可以有效的幫助大家掌握知識、理解原理。

當然你也可以拿去查漏補缺,提升自身的競爭力。

相信它會給大家帶來很多收穫。如果你有需要的話,可以點選獲取

如果你覺得自己學習效率低,缺乏正確的指導,可以加入資源豐富,學習氛圍濃厚的技術圈一起學習交流吧!

喜歡本文的話,不妨順手給我點個贊、評論區留言或者轉發支援一下唄~