-
Activity:在Android應用中負責與用戶交互的組件。
-
Service:常用於爲其他組件提供後臺服務或者監控其他組件的運行狀態。經常用來執行一些耗時操作。
-
BroadcastReceiver:用於監聽應用程序中的其他組件。
-
ContentProvider:Android應用程序之間實現實時數據交換。
1、Activity的生命週期
生命週期:對象什麼時候生,什麼時候死,怎麼寫代碼,代碼往那裏寫。
注意:
-
當打開新的Activity,採用透明主題的時候,當前Activity不會回調onStop
-
onCreate和onDestroy配對,onStart和onStop配對(是否可見),onResume和onPause配對(是否在前臺,可以與用戶交互)
-
打開新的Activity的時候,相關的Log爲:
Main1Activity: onPause
Main2Activity: onCreate
Main2Activity: onStart
Main2Activity: onResume
MainA1ctivity: onStop
異常狀態下的生命週期:
資源相關的系統配置發生改變或者資源不足:例如屏幕旋轉,當前Activity會銷燬,並且在onStop之前回調onSaveInstanceState保存數據,在重新創建Activity的時候在onStart之後回調onRestoreInstanceState。其中Bundle數據會傳到onCreate(不一定有數據)和onRestoreInstanceState(一定有數據)。
防止屏幕旋轉的時候重建,在清單文件中添加配置:
android:configChanges="orientation"
2、Fragment的生命週期
正常啓動
Activity: onCreate
Fragment: onAttach
Fragment: onCreate
Fragment: onCreateView
Fragment: onActivityCreated
Activity: onStart
Activity: onResume
正常退出
Activity: onPause
Activity: onStop
Fragment: onDestroyView
Fragment: onDestroy
Fragment: onDetach
Activity: onDestroy
3、Activity的啓動模式
-
standard:每次激活Activity時(startActivity),都創建Activity實例,並放入任務棧;
-
singleTop:如果某個Activity自己激活自己,即任務棧棧頂就是該Activity,則不需要創建,其餘情況都要創建Activity實例;
-
singleTask:如果要激活的那個Activity在任務棧中存在該實例,則不需要創建,只需要把此Activity放入棧頂,即把該Activity以上的Activity實例都pop,並調用其onNewIntent;
-
singleInstance:應用1的任務棧中創建了MainActivity實例,如果應用2也要激活MainActivity,則不需要創建,兩應用共享該Activity實例。
4、Activity與Fragment之間的傳值
-
通過findFragmentByTag或者getActivity獲得對方的引用(強轉)之後,再相互調用對方的public方法,但是這樣做一是引入了“強轉”的醜陋代碼,另外兩個類之間各自持有對方的強引用,耦合較大,容易造成內存泄漏。
-
通過Bundle的方法進行傳值,例如以下代碼:
//Activity中對fragment設置一些參數
fragment.setArguments(bundle);
//fragment中通過getArguments獲得Activity中的方法
Bundle arguments = getArguments()
-
利用eventbus進行通信,這種方法實時性高,而且Activity與Fragment之間可以完全解耦。
//Activity中的代碼
EventBus.getDefault().post("消息");
//Fragment中的代碼
EventBus.getDefault().register(this);
@Subscribe
public void test(String text) {
tv_test.setText(text);
}
5、Service
-
本地服務,屬於同一個應用程序,通過startService來啓動或者通過bindService來綁定並且獲取代理對象。如果只是想開個服務在後臺運行的話,直接startService即可,如果需要相互之間進行傳值或者操作的話,就應該通過bindService。
-
遠程服務(不同應用程序之間),通過bindService來綁定並且獲取代理對象。
context.startService() ->onCreate()- >onStartCommand()->Service running--調用context.stopService() ->onDestroy()
context.bindService()->onCreate()->onBind()->Service running--調用>onUnbind() -> onDestroy()
Service默認是運行在main線程的,因此Service中如果需要執行耗時操作(大文件的操作,數據庫的拷貝,網絡請求,文件下載等)的話應該在子線程中完成。
!特殊情況是:Service在清單文件中指定了在其他進程中運行。
6、Android中的消息傳遞機制
因爲屏幕的刷新頻率是60Hz,大概16毫秒會刷新一次,所以爲了保證UI的流暢性,耗時操作需要在子線程中處理,子線程不能直接對UI進行更新操作。因此需要Handler在子線程發消息給主線程來更新UI。
這裏再深入一點,Android中的UI控件不是線程安全的,因此在多線程併發訪問UI的時候會導致UI控件處於不可預期的狀態。Google不通過鎖的機制來處理這個問題是因爲:
-
引入鎖會導致UI的操作變得複雜
-
引入鎖會導致UI的運行效率降低
因此,Google的工程師最後是通過單線程的模型來操作UI,開發者只需要通過Handler在不同線程之間切花就可以了。
概述一下Android中的消息機制?
Android中的消息機制主要是指Handler的運行機制。Handler是進行線程切換的關鍵,在主線程和子線程之間切換隻是一種比較特殊的使用情景而已。其中消息傳遞機制需要了解的東西有Message、Handler、Looper、Looper裏面的MessageQueue對象。
如上圖所示,我們可以把整個消息機制看作是一條流水線。其中:
-
MessageQueue是傳送帶,負責Message隊列的傳送與管理
-
Looper是流水線的發動機,不斷地把消息從消息隊列裏面取出來,交給Handler來處理
-
Message是每一件產品
-
Handler就是工人。但是這麼比喻不太恰當,因爲發送以及最終處理Message的都是Handler
Handler的工作是依賴於Looper的,而Looper(與消息隊列)又是屬於某一個線程(ThreadLocal是線程內部的數據存儲類,通過它可以在指定線程中存儲數據,其他線程則無法獲取到),其他線程不能訪問。因此Handler就是間接跟線程是綁定在一起了。因此要使用Handler必須要保證Handler所創建的線程中有Looper對象並且啓動循環。因爲子線程中默認是沒有Looper的,所以會報錯。
正確的使用方法是:
handler = null;
new Thread(new Runnable() {
private Looper mLooper;
@Override
public void run() {
//必須調用Looper的prepare方法爲當前線程創建一個Looper對象,然後啓動循環
//prepare方法中實質是給ThreadLocal對象創建了一個Looper對象
//如果當前線程已經創建過Looper對象了,那麼會報錯
Looper.prepare();
handler = new Handler();
//獲取Looper對象
mLooper = Looper.myLooper();
//啓動消息循環
Looper.loop();
//在適當的時候退出Looper的消息循環,防止內存泄漏
mLooper.quit();
}
}).start();
主線程中默認是創建了Looper並且啓動了消息的循環的,因此不會報錯:
應用程序的入口是ActivityThread的main方法,在這個方法裏面會創建Looper,並且執行Looper的loop方法來啓動消息的循環,使得應用程序一直運行。
子線程中可以通過Handler發送消息給主線程嗎?
可以。有時候出於業務需要,主線程可以向子線程發送消息。子線程的Handler必須按照上述方法創建,並且關聯Looper。
7、事件傳遞機制以及自定義View相關
Android中View的機制主要是Activity的顯示,每個Activity都有一個Window(具體在手機中的實現類是PhoneWindow),Window以下有DecorView,DecorView下面有TitleVie以及ContentView,而ContentView就是我們在Activity中通過setContentView指定的。
ViewGroup有以下三個與事件分發的方法,而View只有dispatchTouchEvent和onTouchEvent。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
事件總是從上往下進行分發,即先到達Activity,再到達ViewGroup,再到達子View,如果沒有任何視圖消耗事件的話,事件會順着路徑往回傳遞。其中:
-
dispatchTouchEvent是事件的分發方法,如果事件能夠到達該視圖的話,就首先一定會調用,一般我們不會去修改這個方法。
-
onInterceptTouchEvent是事件分發的核心方法,表示ViewGroup是否攔截事件,如果返回true表示攔截,在這之後ViewGroup的onTouchEvent會被調用,事件就不會往下傳遞。
-
onTouchEvent是最低級的,在事件分發中最後被調用。
-
子View可以通過requestDisallowInterceptTouchEvent方法去請求父元素不要攔截。
-
事件從Activity.dispatchTouchEvent()開始傳遞,只要沒有被停止或攔截,從最上層的View(ViewGroup)開始一直往下(子View)傳遞。子View 可以通過onTouchEvent()對事件進行處理。
-
事件由父View(ViewGroup)傳遞給子View,ViewGroup 可以通過onInterceptTouchEvent()對事件做攔截,停止其往下傳遞。
-
如果事件從上往下傳遞過程中一直沒有被停止,且最底層子View 沒有消費事件,事件會反向往上傳遞,這時父View(ViewGroup)可以進行消費,如果還是沒有被消費的話,最後會到Activity 的onTouchEvent()函數。
-
如果View 沒有對ACTION_DOWN 進行消費,之後的其他事件不會傳遞過來。
-
OnTouchListener 優先於onTouchEvent()對事件進行消費。
-
對現有的View的子類進行擴展,例如複寫onDraw方法、擴展新功能等。
-
自定義組合控件,把常用一些控件組合起來以方便使用。
-
直接繼承View實現View的完全定製,需要完成View的測量以及繪製。
-
自定義ViewGroup,需要複寫onLayout完成子View位置的確定等工作。
View的測量最終是在onMeasure方法中通過setMeasuredDimension把代表寬高兩個MeasureSpec設置給View,因此需要掌握MeasureSpec。MeasureSpec包括大小信息以及模式信息。
MeasureSpec的三種模式:-
EXACTLY模式:精確模式,對應於用戶指定爲match_parent或者具體大小的時候(實際上指定爲match_parent實質上是指定大小爲父容器的大小)
-
AT_MOST模式:對應於用戶指定爲wrap_content,此時控件尺寸只要不超過父控件允許的最大尺寸即可。
-
UNSPECIFIED模式:不指定大小的測量模式,這種模式比較少用
下面給出模板代碼:
public class MeasureUtils {
/**
* 用於View的測量
*
* @param measureSpec
* @param defaultSize
* @return
*/
public static int measureView(int measureSpec, int defaultSize) {
int measureSize;
//獲取用戶指定的大小以及模式
int mode = View.MeasureSpec.getMode(measureSpec);
int size = View.MeasureSpec.getSize(measureSpec);
//根據模式去返回大小
if (mode == View.MeasureSpec.EXACTLY) {
//精確模式(指定大小以及match_parent)直接返回指定的大小
measureSize = size;
} else {
//UNSPECIFIED模式、AT_MOST模式(wrap_content)的話需要提供默認的大小
measureSize = defaultSize;
if (mode == View.MeasureSpec.AT_MOST) {
//AT_MOST(wrap_content)模式下,需要取測量值與默認值的最小值
measureSize = Math.min(measureSize, defaultSize);
}
}
return measureSize;
}
}
最後,複寫onMeasure方法,把super方法去掉:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(MeasureUtils.measureView(widthMeasureSpec, 200),
MeasureUtils.measureView(heightMeasureSpec, 200)
);
}
View繪製,需要掌握Android中View的座標體系:
View的座標體系是以左上角爲座標原點,向右爲X軸正方向,向下爲Y軸正方向。
View繪製,主要是通過Android的2D繪圖機制來完成,時機是onDraw方法中,其中包括畫布Canvas,畫筆Paint。下面給出示例代碼。相關API不是介紹的重點,重點是Canvas的save和restore方法,通過save以後可以對畫布進行一些放大縮小旋轉傾斜等操作,這兩個方法一般配套使用,其中save的調用次數可以多於restore。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = ImageUtils.drawable2Bitmap(mDrawable);
canvas.drawBitmap(bitmap, getLeft(), getTop(), mPaint);
canvas.save();
//注意,這裏的旋轉是指畫布的旋轉
canvas.rotate(90);
mPaint.setColor(Color.parseColor("#FF4081"));
mPaint.setTextSize(30);
canvas.drawText("測試", 100, -100, mPaint);
canvas.restore();
}
View的位置-onLayout與佈局位置相關的是onLayout方法的複寫,一般我們自定義View的時候,只需要完成測量,繪製即可。如果是自定義ViewGroup的話,需要做的就是在onLayout中測量自身以及控制子控件的佈局位置,onLayout是自定義ViewGroup必須實現的方法。
8、性能優化
-
使用include標籤,通過layout屬性複用相同的佈局。
<include
android:id="@+id/v_test"
layout="@layout/include_view" /
-
使用merge標籤,去除同類的視圖
-
使用ViewStub來進行佈局的延遲加載一些不是馬上就用到的佈局。例如列表頁中,列表在沒有拿到數據之前不加載,這樣做可以使UI變得流暢。
<ViewStub
android:id="@+id/v_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/view_stub" />
//需要手動調用inflate方法,佈局纔會顯示出來。
stub.inflate();
//其中setVisibility在底層也是會調用inflate方法
//stub.setVisibility(View.VISIBLE);
//之後,如果要使用ViewStub標籤裏面的View,只需要按照平常來即可。
TextView tv_1 = (TextView) findViewById(R.id.tv_1);
-
儘量多使用RelativeLayout,因爲這樣可以大大減少視圖的層級。
內存優化
-
珍惜Service,儘量使得Service在使用的時候才處於運行狀態。儘量使用IntentService
IntentService在內部其實是通過線程以及Handler實現的,當有新的Intent到來的時候,會創建線程並且處理這個Intent,處理完畢以後就自動銷燬自身。因此使用IntentService能夠節省系統資源。
-
內存緊張的時候釋放資源(例如UI隱藏的時候釋放資源等)。複寫Activity的回調方法。
@Override
public void onLowMemory() {
super.onLowMemory();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
case TRIM_MEMORY_COMPLETE:
//...
break;
case 其他:
}
}
-
通過Manifest中對Application配置更大的內存,但是一般不推薦
android:largeHeap="true"
-
避免Bitmap的浪費,應該儘量去適配屏幕設備。儘量使用成熟的圖片加載框架,Picasso,Fresco,Glide等。
-
使用優化的容器,SparseArray等
-
其他建議:儘量少用枚舉變量,儘量少用抽象,儘量少增加類,避免使用依賴注入框架,謹慎使用library,使用代碼混淆,時當場合考慮使用多進程等。
-
避免內存泄漏(本來應該被回收的對象沒有被回收)。一旦APP的內存短時間內快速增長或者GC非常頻繁的時候,就應該考慮是否是內存泄漏導致的。
分析方法1. 使用Android Studio提供的Android Monitors中Memory工具查看內存的使用以及沒使用的情況。
2. 使用DDMS提供的Heap工具查看內存使用情況,也可以手動觸發GC。
3. 使用性能分析的依賴庫,例如Square的LeakCanary,這個庫會在內存泄漏的前後通過Notification通知你。
-
資源釋放問題:程序代碼的問題,長期保持某些資源,如Context、Cursor、IO 流的引用,資源得不到釋放造成內存泄露。
-
對象內存過大問題:保存了多個耗用內存過大的對象(如Bitmap、XML 文件),造成內存超出限制。
-
static 關鍵字的使用問題:static 是Java 中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬於該類,而不是該類的實例。所以用static 修飾的變量,它的生命週期是很長的,如果用它來引用一些資源耗費過多的實例(Context 的情況最多),這時就要謹慎對待了。
解決方案
1. 應該儘量避免static 成員變量引用資源耗費過多的實例,比如Context。
2. Context 儘量使用ApplicationContext,因爲Application 的Context 的生命週期比較長,引用它不會出現內存泄露的問題。
3. 使用WeakReference 代替強引用。比如可以使用WeakReference<Context> mContextRef -
線程導致內存溢出:線程產生內存泄露的主要原因在於線程生命週期的不可控。例如Activity中的Thread在run了,但是Activity由於某種原因重新創建了,但是Thread仍然會運行,因爲run方法不結束的話Thread是不會銷燬的。
解決方案1. 將線程的內部類,改爲靜態內部類(因爲非靜態內部類擁有外部類對象的強引用,而靜態類則不擁有)。
2. 在線程內部採用弱引用保存Context 引用。
-
android官方提供的工具:Memory Monitor(當APP佔用的內存在短時間內快速增長或者GC變得頻繁的時候)、DDMS提供的Heap工具(手動觸發GC)
-
Square提供的內存泄漏檢測工具,LeakCanary(能夠自動完成內存追蹤、檢測、輸出結果),進行演示,並且適當的解說。