做了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_STICKY、START_STICKY和START_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主要有以下幾個步驟:
- 創建待添加的fragment實體,
- 獲取FragmentManager,在Activity中可以直接通過呼叫 getSupportFragmentManager()方法得到,
- 開啟一個事務,通過呼叫beginTransaction()方法開啟,
- 向容器內添加或替換fragment,一般使用repalce()方法實作,需要傳入容器的id和待添加的fragment實體,
- 提交事務,呼叫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,
- 事件從左上角那個白色箭頭開始,由Activity的dispatchTouchEvent做分發
- 箭頭的上面字代表方法回傳值,(return true、return false、return super.xxxxx(),super 的意思是呼叫父類實作,
- dispatchTouchEvent和 onTouchEvent的框里有個【true---->消費】的字,表示的意思是如果方法回傳true,那么代表事件就此消費,不會繼續往別的地方傳了,事件終止,
- 目前所有的圖的事件是針對ACTION_DOWN的,對于ACTION_MOVE和ACTION_UP我們最后做分析,
- 之前圖中的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有三種測量模式:
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放入指定的位置,
-
首先,我們得知道各個子View的大小吧,只有先知道子View的大小,我們才知道當前的ViewGroup該設定為多大去容納它們,
-
根據子View的大小,以及我們的ViewGroup要實作的功能,決定出ViewGroup的大小
-
ViewGroup和子View的大小算出來了之后,接下來就是去擺放了吧,具體怎么去擺放呢?這得根據你定制的需求去擺放了,比如,你想讓子View按照垂直順序一個挨著一個放,或者是按照先后順序一個疊一個去放,這是你自己決定的,
-
已經知道怎么去擺放還不行啊,決定了怎么擺放就是相當于把已有的空間”分割”成大大小小的空間,每個空間對應一個子View,我們接下來就是把子View對號入座了,把它們放進它們該放的地方去,


3、系統原理
3.1、打包原理
Android的包檔案APK分為兩個部分:代碼和資源,所以打包方面也分為資源打包和代碼打包兩個方面,這篇文章就來分析資源和代碼的編譯打包原理,
具體說來:
- 通過AAPT工具進行資源檔案(包括AndroidManifest.xml、布局檔案、各種xml資源等)的打包,生成R.java檔案,
- 通過AIDL工具處理AIDL檔案,生成相應的Java檔案,
- 通過Javac工具編譯專案原始碼,生成Class檔案,
- 通過DX工具將所有的Class檔案轉換成DEX檔案,該程序主要完成Java位元組碼轉換成Dalvik位元組碼,壓縮常量池以及清除冗余資訊等作業,
- 通過ApkBuilder工具將資源檔案、DEX檔案打包生成APK檔案,
- 利用KeyStore對生成的APK檔案進行簽名,
- 如果是正式版的APK,還會利用ZipAlign工具進行對齊處理,對齊的程序就是將APK檔案中所有的資源檔案舉例檔案的起始距離都偏移4位元組的整數倍,這樣通過記憶體映射訪問APK檔案的速度會更快,

3.2、安裝流程
Android apk的安裝程序主要氛圍以下幾步:
- 復制APK到/data/app目錄下,解壓并掃描安裝包,
- 資源管理器決議APK里的資源檔案,
- 決議AndroidManifest檔案,并在/data/data/目錄下創建對應的應用資料目錄,
- 然后對dex檔案進行優化,并保存在dalvik-cache目錄下,
- 將AndroidManifest檔案決議出的四大組件資訊注冊到PackageManagerService中,
- 安裝完成后,發送廣播,
可以使用下面的圖表示:

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執行的原理如下:
- 首先,通過method把它轉換成ServiceMethod,
- 然后,通過serviceMethod,args獲取到okHttpCall物件,
- 最后,再把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)由來:
- APP版本迭代,新功能不斷增加,業務變得復雜,維護成本高
- 業務耦合度高,代碼臃腫,團隊內部多人協作開發困難
- Android編譯代碼卡頓,單一工程下代碼耦合嚴重,修改一處需要重新編譯打包,耗時耗力,
- 方便單元測驗,單獨改一個業務模塊,不需要著重關注其他模塊,
(3)優勢:
- 組件化將通用模塊獨立出來,統一管理,以提高復用,將頁面拆分為粒度更小的組件,組件內部出了包含UI實作,還可以包含資料層和邏輯層
- 每個組件度可以獨立編譯、加快編譯速度、獨立打包,
- 每個工程內部的修改,不會影響其他工程,
- 業務庫工程可以快速拆分出來,集成到其他App中,
- 迭代頻繁的業務模塊采用組件方式,業務線研發可以互不干擾、提升協作效率,并控制產品質量,加強穩定性,
- 并行開發,團隊成員只關注自己的開發的小模塊,降低耦合性,后期維護方便等,
(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編程思想中的類的初始化程序主要有以下幾點:
- 找到class檔案,將它加載到記憶體
- 在堆記憶體中分配記憶體地址
- 初始化
- 將堆記憶體地址指給堆疊記憶體中的p變數
2、String、StringBuilder、StringBuffer
StringBuffer里面的很多方法添加了synchronized關鍵字,是可以表征執行緒安全的,所以多執行緒情況下使用它,
執行速度:
StringBuilder > StringBuffer > String
StringBuilder犧牲了性能來換取速度的,這兩個是可以直接在原物件上面進行修改,省去了創建新物件和回收老物件的程序,而String是字串常量(final)修試,另外兩個是字串變數,常量物件一旦創建就不可以修改,變數是可以進行修改的,所以對于String字串的操作包含下面三個步驟:
- 創建一個新物件,名字和原來的一樣
- 在新物件上面進行修改
- 原物件被垃圾回收掉
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機制
垃圾收集器一般完成兩件事
- 檢測出垃圾;
- 回收垃圾;
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、單例設計模式
單例主要分為:懶漢式單例、餓漢式單例、登記式單例,
特點:
- 單例類只有一個實體
- 單例類必須自己創建自己的唯一實體
- 單例類必須給所有其他物件提供這一實體,
在計算機系統中,像執行緒池,快取、日志物件、對話框、列印機等常被設計成單例,
懶漢式單例:
Singleton通過將構造方法限定為private避免了類在外部被實體化,在同一個虛擬機范圍內,Singleton的唯一實體只能通過getInstance()方法訪問,(事實上,通過Java反射機制是能夠實體化構造方法為private的類的,那基本上會使所有的Java單例實作失效,

它是執行緒不安全的,并發情況下很有可能出現多個Singleton實體,要實作執行緒安全,有以下三種方式: 1.在getInstance方法上加上同步
2.雙重檢查鎖定
3.靜態內部類
這種方式對比前兩種,既實作了執行緒安全,又避免了同步帶來的性能影響,
餓漢式單例:
餓漢式在創建類的同時就已經創建好了一個靜態的物件供系統使用,以后不再改變,所以天生是系統安全,

最后
漫漫開發之路,你我只是其中的一小部分……只有不斷的學習、進階,才是我們的出路!才跟得上時代的進步!
如果你看到了這里,覺得文章寫得不錯就給個贊唄?如果你覺得那里值得改進的,請給我留言,一定會認真查詢,修正不足,謝謝,
希望讀到這的您能轉發分享和關注一下我,以后還會更新技術干貨,謝謝您的支持!
有一句老話說的好:**“比你優秀的對手在學習,你的仇人在磨刀,你的閨蜜在減肥,隔壁老王在練腰,我們必須不斷學習,否則我們將被學習者超越,”**當然一個人學習是枯燥的,還需要一個良好的學習氛圍,因此我組建了一個學習交流探討的社群,歡迎大家一起來交流探討共同進步,還有一些收集整理的資料,感興趣的可以加群,一起學習,共同進步!
這邊把我收錄整理的一些資料拿出來分享給大家,一方面是希望能夠幫助大家提高,一方面也是警醒自己,要不斷學習、不斷提升,進階才是王道!
分享給大家的資料包括 高級架構技術進階腦圖、 Android開發面試專題資料,還有 高級進階架構資料包括但不限于 【高級UI、性能優化、移動架構師、NDK、混合式開發(ReactNative+Weex)微信小程式、Flutter等全方面的Android進階實踐技術】希望能幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也是可以分享給身邊好友一起學習的!希望能幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也是可以分享給身邊好友一起學習的!
Android架構師之路很漫長,一起共勉吧!
面試難免讓人焦慮不安,經歷過的人都懂的,但是如果你提前預測面試官要問你的問題并想出得體的回答方式,就會容易很多,
上述面試題答案都整理成檔案筆記, 也還整理了一些Android學習PDF+架構視頻+原始碼筆記,高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料
這些都是我現在閑暇還會反復翻閱的精品資料,里面對近幾年的大廠面試高頻知識點都有詳細的講解,相信可以有效的幫助大家掌握知識、理解原理,
當然你也可以拿去查漏補缺,提升自身的競爭力,
相信它會給大家帶來很多識訓,如果你有需要的話,可以點擊獲取!
如果你覺得自己學習效率低,缺乏正確的指導,可以加入資源豐富,學習氛圍濃厚的技術圈一起學習交流吧!
喜歡本文的話,不妨順手給我點個贊、評論區留言或者轉發支持一下唄~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/179684.html
標籤:其他
上一篇:gradle常見問題
