本次的學習指南系列將分為三個模塊
- 第一 Java知識點匯總
- 第二 Android基礎知識點匯總(一)(二)
- 第三 Android進階知識點匯總
后續的NDK、跨平臺、底層原始碼等技術,也會抽空給大家整理一下知識點,如果喜歡的話,希望大家給個關注,點個贊唄,個人主頁簡介有聯系方式,可找我拿PDF版本的學習指南,
怎么聯系我:Android.md · master · 讓開,我要吃人了 / Android · CODE CHINACODE CHINA——開源代碼托管平臺,獨立第三方開源社區,Git/Github/Gitlab
https://codechina.csdn.net/weixin_55362248/android/-/blob/master/Android.md
Activity
生命周期

- Activity A 啟動另一個Activity B,回呼如下:
Activity A 的onPause() → Activity B的onCreate() → onStart() → onResume() → Activity A的onStop();如果B是透明主題又或則是個DialogActivity,則不會回呼A的onStop; - 使用onSaveInstanceState()保存簡單,輕量級的UI狀態
lateinit var textView: TextView
var gameState: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
gameState = savedInstanceState?.getString(GAME_STATE_KEY)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.text_view)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
textView.text = savedInstanceState?.getString(TEXT_VIEW_KEY)
}
override fun onSaveInstanceState(outState: Bundle?) {
outState?.run {
putString(GAME_STATE_KEY, gameState)
putString(TEXT_VIEW_KEY, textView.text.toString())
}
super.onSaveInstanceState(outState)
}
啟動模式
| LaunchMode | 說明 |
|---|---|
| standard | 系統在啟動它的任務中創建 activity 的新實體 |
| singleTop | 如果activity的實體已存在于當前任務的頂部,則系統通過呼叫其onNewIntent(),否則會創建新實體 |
| singleTask | 系統創建新 task 并在 task 的根目錄下實體化 activity,但如果 activity 的實體已存在于單獨的任務中,則呼叫其 onNewIntent() 方法,其上面的實體會被移除堆疊,一次只能存在一個 activity 實體 |
| singleInstance | 相同 singleTask,activity始終是其task的唯一成員; 任何由此開始的activity 都在一個單獨的 task 中打開 |
啟動程序

ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
//step 1: 創建LoadedApk物件
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
... //component初始化程序
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
//step 2: 創建Activity物件
Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
...
//step 3: 創建Application物件
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
//step 4: 創建ContextImpl物件
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
//step5: 將Application/ContextImpl都attach到Activity物件
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
...
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}
activity.mCalled = false;
if (r.isPersistable()) {
//step 6: 執行回呼onCreate
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
r.activity = activity;
r.stopped = true;
if (!r.activity.mFinished) {
activity.performStart(); //執行回呼onStart
r.stopped = false;
}
if (!r.activity.mFinished) {
//執行回呼onRestoreInstanceState
if (r.isPersistable()) {
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
...
r.paused = true;
mActivities.put(r.token, r);
}
return activity;
}
Fragment
特點
- Fragment 解決 Activity 間的切換不流暢,輕量切換
- 可以從 startActivityForResult 中接收到回傳結果,但是View不能
- 只能在 Activity 保存其狀態(用戶離開 Activity)之前使用 commit() 提交事務,如果您試圖在該時間點后提交,則會引發例外, 這是因為如需恢復 Activity,則提交后的狀態可能會丟失, 對于丟失提交無關緊要的情況,請使用 commitAllowingStateLoss(),
生命周期


與Activity通信
執行此操作的一個好方法是,在片段內定義一個回呼介面,并要求宿主 Activity 實作它,
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString());
}
}
...
}
Service
Service 分為兩種作業狀態,一種是啟動狀態,主要用于執行后臺計算;另一種是系結狀態,主要用于其他組件和 Service 的互動,
啟動程序

ActivityThread.java
@UnsupportedAppUsage
private void handleCreateService(CreateServiceData data) {
···
LoadedApk packageInfo = getPackageInfoNoCheck(
data.info.applicationInfo, data.compatInfo);
Service service = null;
try {
java.lang.ClassLoader cl = packageInfo.getClassLoader();
service = packageInfo.getAppFactory()
.instantiateService(cl, data.info.name, data.intent);
}
···
try {
if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);
ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
context.setOuterContext(service);
Application app = packageInfo.makeApplication(false, mInstrumentation);
service.attach(context, this, data.info.name, data.token, app,
ActivityManager.getService());
service.onCreate();
mServices.put(data.token, service);
try {
ActivityManager.getService().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
···
}
系結程序

ActivityThread.java
private void handleBindService(BindServiceData data) {
Service s = mServices.get(data.token);
···
if (s != null) {
try {
data.intent.setExtrasClassLoader(s.getClassLoader());
data.intent.prepareToEnterProcess();
try {
if (!data.rebind) {
IBinder binder = s.onBind(data.intent);
ActivityManager.getService().publishService(
data.token, data.intent, binder);
} else {
s.onRebind(data.intent);
ActivityManager.getService().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
}
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
···
}
}
生命周期

| 值 | 說明 |
|---|---|
| START_NOT_STICKY | 如果系統在 onStartCommand() 回傳后終止服務,則除非有掛起 Intent 要傳遞,否則系統不會重建服務,這是最安全的選項,可以避免在不必要時以及應用能夠輕松重啟所有未完成的作業時運行服務 |
| START_STICKY | 如果系統在 onStartCommand() 回傳后終止服務,則會重建服務并呼叫 onStartCommand(),但不會重新傳遞最后一個 Intent,相反,除非有掛起 Intent 要啟動服務(在這種情況下,將傳遞這些 Intent ),否則系統會通過空 Intent 呼叫 onStartCommand(),這適用于不執行命令、但無限期運行并等待作業的媒體播放器(或類似服務 |
| START_REDELIVER_INTENT | 如果系統在 onStartCommand() 回傳后終止服務,則會重建服務,并通過傳遞給服務的最后一個 Intent 呼叫 onStartCommand(),任何掛起 Intent 均依次傳遞,這適用于主動執行應該立即恢復的作業(例如下載檔案)的服務 |
啟用前臺服務
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
Notification notification = new Notification(icon, text, System.currentTimeMillis());
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(this, title, mmessage, pendingIntent);
startForeground(ONGOING_NOTIFICATION_ID, notification);
BroadcastReceiver
target 26 之后,無法在 AndroidManifest 顯示宣告大部分廣播,除了一部分必要的廣播,如:
- ACTION_BOOT_COMPLETED
- ACTION_TIME_SET
- ACTION_LOCALE_CHANGED
LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(receiver, filter);
注冊程序
ContentProvider
ContentProvider 管理對結構化資料集的訪問,它們封裝資料,并提供用于定義資料安全性的機制, 內容提供程式是連接一個行程中的資料與另一個行程中運行的代碼的標準界面,
ContentProvider 無法被用戶感知,對于一個 ContentProvider 組件來說,它的內部需要實作增刪該查這四種操作,它的內部維持著一份資料集合,這個資料集合既可以是資料庫實作,也可以是其他任何型別,如 List 和 Map,內部的 insert、delete、update、query 方法需要處理好執行緒同步,因為這幾個方法是在 Binder 執行緒池中被呼叫的,
ContentProvider 通過 Binder 向其他組件乃至其他應用提供資料,當 ContentProvider 所在的行程啟動時,ContentProvider 會同時啟動并發布到 AMS 中,需要注意的是,這個時候 ContentProvider 的 onCreate 要先于 Application 的 onCreate 而執行,
基本使用
// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // The content URI of the words table
mProjection, // The columns to return for each row
mSelectionClause // Selection criteria
mSelectionArgs, // Selection criteria
mSortOrder); // The sort order for the returned rows
public class Installer extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
ContentProvider 和 sql 在實作上有什么區別?
- ContentProvider 屏蔽了資料存盤的細節,內部實作透明化,用戶只需關心 uri 即可(是否匹配)
- ContentProvider 能實作不同 app 的資料共享,sql 只能是自己程式才能訪問
- Contentprovider 還能增刪本地的檔案,xml等資訊
資料存盤
| 存盤方式 | 說明 |
|---|---|
| SharedPreferences | 在鍵值對中存盤私有原始資料 |
| 內部存盤 | 在設備記憶體中存盤私有資料 |
| 外部存盤 | 在共享的外部存盤中存盤公共資料 |
| SQLite 資料庫 | 在私有資料庫中存盤結構化資料 |
View
ViewRoot 對應于 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View 的三大流程均是通過 ViewRoot 來完成的,在 ActivityThread 中,當 Activity 物件被創建完畢后,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 物件,并將 ViewRootImpl 物件和 DecorView 建立關聯
View 的整個繪制流程可以分為以下三個階段:
- measure: 判斷是否需要重新計算 View 的大小,需要的話則計算
- layout: 判斷是否需要重新計算 View 的位置,需要的話則計算
- draw: 判斷是否需要重新繪制 View,需要的話則重繪制

MeasureSpec
MeasureSpec表示的是一個32位的整形值,它的高2位表示測量模式SpecMode,低30位表示某種測量模式下的規格大小SpecSize,MeasureSpec 是 View 類的一個靜態內部類,用來說明應該如何測量這個 View
| Mode | 說明 |
|---|---|
| UNSPECIFIED | 不指定測量模式, 父視圖沒有限制子視圖的大小,子視圖可以是想要的任何尺寸,通常用于系統內部,應用開發中很少用到, |
| EXACTLY | 精確測量模式,視圖寬高指定為 match_parent 或具體數值時生效,表示父視圖已經決定了子視圖的精確大小,這種模式下 View 的測量值就是 SpecSize 的值 |
| AT_MOST | 最大值測量模式,當視圖的寬高指定為 wrap_content 時生效,此時子視圖的尺寸可以是不超過父視圖允許的最大尺寸的任何尺寸 |
對于 DecorView 而言,它的MeasureSpec 由視窗尺寸和其自身的 LayoutParams 共同決定;對于普通的 View,它的 MeasureSpec 由父視圖的 MeasureSpec 和其自身的 LayoutParams 共同決定
| childLayoutParams/parentSpecMode | EXACTLY | AT_MOST |
|---|---|---|
| dp/px | EXACTLY(childSize) | EXACTLY(childSize) |
| match_parent | EXACTLY(childSize) | AT_MOST(parentSize) |
| wrap_content | AT_MOST(parentSize) | AT_MOST(parentSize) |
直接繼承 View 的控制元件需要重寫 onMeasure 方法并設定 wrap_content 時的自身大小,因為 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在這種模式下,它的寬/高等于父容器當前剩余的空間大小,就相當于使用 match_parent,這解決方式如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 在 wrap_content 的情況下指定內部寬/高(mWidth 和 mHeight`)
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasureDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasureDimension(widthSpecSize, mHeight);
}
}
MotionEvent
| 事件 | 說明 |
|---|---|
| ACTION_DOWN | 手指剛接觸到螢屏 |
| ACTION_MOVE | 手指在螢屏上移動 |
| ACTION_UP | 手機從螢屏上松開的一瞬間 |
| ACTION_CANCEL | 觸摸事件取消 |
點擊螢屏后松開,事件序列為 DOWN -> UP,點擊螢屏滑動松開,事件序列為 DOWN -> MOVE -> ...> MOVE -> UP,
getX/getY 回傳相對于當前View左上角的坐標,getRawX/getRawY 回傳相對于螢屏左上角的坐標
TouchSlop是系統所能識別出的被認為滑動的最小距離,不同設備值可能不相同,可通過 ViewConfiguration.get(getContext()).getScaledTouchSlop() 獲取,
VelocityTracker
VelocityTracker 可用于追蹤手指在滑動中的速度:
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
velocityTracker.clear();
velocityTracker.recycle();
return false;
}
});
GestureDetector
GestureDetector 輔助檢測用戶的單擊、滑動、長按、雙擊等行為:
final GestureDetector mGestureDetector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) { return false; }
@Override
public void onShowPress(MotionEvent e) { }
@Override
public boolean onSingleTapUp(MotionEvent e) { return false; }
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }
@Override
public void onLongPress(MotionEvent e) { }
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
});
mGestureDetector.setOnDoubleTapListener(new OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) { return false; }
@Override
public boolean onDoubleTap(MotionEvent e) { return false; }
@Override
public boolean onDoubleTapEvent(MotionEvent e) { return false; }
});
// 解決長按螢屏后無法拖動的問題
mGestureDetector.setIsLongpressEnabled(false);
imageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
});
如果是監聽滑動相關,建議在 onTouchEvent 中實作,如果要監聽雙擊,那么就使用 GestureDectector,
Scroller
彈性滑動物件,用于實作 View 的彈性滑動,Scroller 本身無法讓 View 彈性滑動,需要和 View 的 computeScroll 方法配合使用,startScroll方法是無法讓 View 滑動的,invalidate 會導致 View 重繪,重回后會在 draw 方法中又會去呼叫 computeScroll 方法,computeScroll 方法又會去向 Scroller 獲取當前的 scrollX 和 scrollY,然后通過 scrollTo 方法實作滑動,接著又呼叫 postInvalidate 方法如此反復,
Scroller mScroller = new Scroller(mContext);
private void smoothScrollTo(int destX) {
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms 內滑向 destX,效果就是慢慢滑動
mScroller.startScroll(scrollX, 0 , delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
View 的滑動
scrollTo/scrollBy
適合對 View 內容的滑動,scrollBy實際上也是呼叫了scrollTo方法:
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
mScrollX的值等于 View 的左邊緣和 View 內容左邊緣在水平方向的距離,mScrollY的值等于 View 上邊緣和 View 內容上邊緣在豎直方向的距離,scrollTo 和 scrollBy 只能改變 View 內容的位置而不能改變 View 在布局中的位置,
- 使用影片
操作簡單,主要適用于沒有互動的 View 和實作復雜的影片效果, - 改變布局引數 操作稍微復雜,適用于有互動的 View.
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
view.requestLayout();
//或者 view.setLayoutParams(params);
View 的事件分發
點擊事件達到頂級 View(一般是一個 ViewGroup),會呼叫 ViewGroup 的 dispatchTouchEvent 方法,如果頂級 ViewGroup 攔截事件即 onInterceptTouchEvent 回傳 true,則事件由 ViewGroup 處理,這時如果 ViewGroup 的 mOnTouchListener 被設定,則 onTouch 會被呼叫,否則 onTouchEvent 會被呼叫,也就是說如果都提供的話,onTouch 會屏蔽掉 onTouchEvent,在 onTouchEvent 中,如果設定了 mOnClickListenser,則 onClick 會被呼叫,如果頂級 ViewGroup 不攔截事件,則事件會傳遞給它所在的點擊事件鏈上的子 View,這時子 View 的 dispatchTouchEvent 會被呼叫,如此回圈,
- ViewGroup 默認不攔截任何事件,ViewGroup 的 onInterceptTouchEvent 方法默認回傳 false,
- View 沒有 onInterceptTouchEvent 方法,一旦有點擊事件傳遞給它,onTouchEvent 方法就會被呼叫,
- View 在可點擊狀態下,onTouchEvent 默認會消耗事件,
- ACTION_DOWN 被攔截了,onInterceptTouchEvent 方法執行一次后,就會留下記號(mFirstTouchTarget == null)那么往后的 ACTION_MOVE 和 ACTION_UP 都會攔截,`
在 Activity 中獲取某個 View 的寬高
- Activity/View#onWindowFocusChanged
// 此時View已經初始化完畢
// 當Activity的視窗得到焦點和失去焦點時均會被呼叫一次
// 如果頻繁地進行onResume和onPause,那么onWindowFocusChanged也會被頻繁地呼叫
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasureWidth();
int height = view.getMeasuredHeight();
}
}
- view.post(runnable)
// 通過post可以將一個runnable投遞到訊息佇列的尾部,// 然后等待Looper呼叫次runnable的時候,View也已經初
// 始化好了
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
- ViewTreeObserver
// 當View樹的狀態發生改變或者View樹內部的View的可見// 性發生改變時,onGlobalLayout方法將被回呼
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
Draw 的基本流程
// 繪制基本上可以分為六個步驟
public void draw(Canvas canvas) {
...
// 步驟一:繪制View的背景
drawBackground(canvas);
...
// 步驟二:如果需要的話,保持canvas的圖層,為fading做準備
saveCount = canvas.getSaveCount();
...
canvas.saveLayer(left, top, right, top + length, null, flags);
...
// 步驟三:繪制View的內容
onDraw(canvas);
...
// 步驟四:繪制View的子View
dispatchDraw(canvas);
...
// 步驟五:如果需要的話,繪制View的fading邊緣并恢復圖層
canvas.drawRect(left, top, right, top + length, p);
...
canvas.restoreToCount(saveCount);
...
// 步驟六:繪制View的裝飾(例如滾動條等等)
onDrawForeground(canvas)
}
自定義 View
- 繼承 View 重寫
onDraw方法
主要用于實作一些不規則的效果,靜態或者動態地顯示一些不規則的圖形,即重寫 onDraw 方法,采用這種方式需要自己支持 wrap_content,并且 padding 也需要自己處理,
- 繼承 ViewGroup 派生特殊的 Layout
主要用于實作自定義布局,采用這種方式需要合適地處理 ViewGroup 的測量、布局兩個程序,并同時處理子元素的測量和布局程序,
- 繼承特定的 View
用于擴張某種已有的View的功能
- 繼承特定的 ViewGroup
用于擴張某種已有的ViewGroup的功能
行程
行程(Process) 是計算機中的程式關于某資料集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是作業系統結構的基礎,
當某個應用組件啟動且該應用沒有運行其他任何組件時,Android 系統會使用單個執行執行緒為應用啟動新的 Linux 行程,默認情況下,同一應用的所有組件在相同的行程和執行緒(稱為“主”執行緒)中運行,
各類組件元素的清單檔案條目<activity>、<service>、<receiver> 和 <provider>—均支持 android:process 屬性,此屬性可以指定該組件應在哪個行程運行,
行程生命周期
1、前臺行程
- 托管用戶正在互動的 Activity(已呼叫 Activity 的
onResume()方法) - 托管某個 Service,后者系結到用戶正在互動的 Activity
- 托管正在“前臺”運行的 Service(服務已呼叫
startForeground()) - 托管正執行一個生命周期回呼的 Service(
onCreate()、onStart()或onDestroy()) - 托管正執行其
onReceive()方法的 BroadcastReceiver
2、可見行程
- 托管不在前臺、但仍對用戶可見的 Activity(已呼叫其
onPause()方法),例如,如果 re前臺 Activity 啟動了一個對話框,允許在其后顯示上一 Activity,則有可能會發生這種情況, - 托管系結到可見(或前臺)Activity 的 Service
3、服務行程
- 正在運行已使用 startService() 方法啟動的服務且不屬于上述兩個更高類別行程的行程,
4、后臺行程
- 包含目前對用戶不可見的 Activity 的行程(已呼叫 Activity 的
onStop()方法),通常會有很多后臺行程在運行,因此它們會保存在 LRU (最近最少使用)串列中,以確保包含用戶最近查看的 Activity 的行程最后一個被終止,
5、空行程
- 不含任何活動應用組件的行程,保留這種行程的的唯一目的是用作快取,以縮短下次在其中運行組件所需的啟動時間, 為使總體系統資源在行程快取和底層內核快取之間保持平衡,系統往往會終止這些行程,\
多行程
如果注冊的四大組件中的任意一個組件時用到了多行程,運行該組件時,都會創建一個新的 Application 物件,對于多行程重復創建 Application 這種情況,只需要在該類中對當前行程加以判斷即可,
public class MyApplication extends Application {
@Override
public void onCreate() {
Log.d("MyApplication", getProcessName(android.os.Process.myPid()));
super.onCreate();
}
/**
* 根據行程 ID 獲取行程名
* @param pid 行程id
* @return 行程名
*/
public String getProcessName(int pid){
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> processInfoList = am.getRunningAppProcesses();
if (processInfoList == null) {
return null;
}
for (ActivityManager.RunningAppProcessInfo processInfo : processInfoList) {
if (processInfo.pid == pid) {
return processInfo.processName;
}
}
return null;
}
}
一般來說,使用多行程會造成以下幾個方面的問題:
- 靜態成員和單例模式完全失效
- 執行緒同步機制完全失效
- SharedPreferences 的可靠性下降
- Application 會多次創建
行程存活
OOM_ADJ
| ADJ級別 | 取值 | 解釋 |
|---|---|---|
| UNKNOWN_ADJ | 16 | 一般指將要會快取行程,無法獲取確定值 |
| CACHED_APP_MAX_ADJ | 15 | 不可見行程的adj最大值 |
| CACHED_APP_MIN_ADJ | 9 | 不可見行程的adj最小值 |
| SERVICE_B_AD | 8 | B List 中的 Service(較老的、使用可能性更小) |
| PREVIOUS_APP_ADJ | 7 | 上一個App的行程(往往通過按回傳鍵) |
| HOME_APP_ADJ | 6 | Home行程 |
| SERVICE_ADJ | 5 | 服務行程(Service process) |
| HEAVY_WEIGHT_APP_ADJ | 4 | 后臺的重量級行程,system/rootdir/init.rc 檔案中設定 |
| BACKUP_APP_ADJ | 3 | 備份行程 |
| PERCEPTIBLE_APP_ADJ | 2 | 可感知行程,比如后臺音樂播放 |
| VISIBLE_APP_ADJ | 1 | 可見行程(Visible process) |
| FOREGROUND_APP_ADJ | 0 | 前臺行程(Foreground process) |
| PERSISTENT_SERVICE_ADJ | -11 | 關聯著系統或persistent行程 |
| PERSISTENT_PROC_ADJ | -12 | 系統 persistent 行程,比如telephony |
| SYSTEM_ADJ | -16 | 系統行程 |
| NATIVE_ADJ | -17 | native行程(不被系統管理) |
行程被殺情況

行程保活方案
- 開啟一個像素的 Activity
- 使用前臺服務
- 多行程相互喚醒
- JobSheduler 喚醒
- 粘性服務 & 與系統服務捆綁
Parcelable 介面
只要實作了 Parcelable 介面,一個類的物件就可以實作序列化并可以通過 Intent 和 Binder 傳遞,
使用示例
import android.os.Parcel;
import android.os.Parcelable;
public class User implements Parcelable {
private int userId;
protected User(Parcel in) {
userId = in.readInt();
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(userId);
}
public int getUserId() {
return userId;
}
}
方法說明
Parcel 內部包裝了可序列化的資料,可以在 Binder 中自由傳輸,序列化功能由 writeToParcel 方法完成,最終是通過 Parcel 中的一系列 write 方法完成,反序列化功能由 CREATOR 來完成,通過 Parcel 的一系列 read 方法來完成反序列化程序,
| 方法 | 功能 |
|---|---|
| createFromParcel(Parcel in) | 從序列化后的物件中創建原始物件 |
| newArray(int size) | 創建指定長度的原始物件陣列 |
| User(Parcel in) | 從序列化后的物件中創建原始物件 |
| writeToParcel(Parcel dest, int flags) | 將當前物件寫入序列化結構中,其中 flags 標識有兩種值:0 或者 1,為 1 時標識當前物件需要作為回傳值回傳,不能立即釋放資源,幾乎所有情況都為 0 |
| describeContents | 回傳當前物件的內容描述,如果含有檔案描述符,回傳 1,否則回傳 0,幾乎所有情況都回傳 0 |
Parcelable 與 Serializable 對比
- Serializable 使用 I/O 讀寫存盤在硬碟上,而 Parcelable 是直接在記憶體中讀寫
- Serializable 會使用反射,序列化和反序列化程序需要大量 I/O 操作, Parcelable 自已實作封送和解封(marshalled &unmarshalled)操作不需要用反射,資料也存放在 Native 記憶體中,效率要快很多
IPC
IPC 即 Inter-Process Communication (行程間通信),Android 基于 Linux,而 Linux 出于安全考慮,不同行程間不能之間操作對方的資料,這叫做“行程隔離”,
在 Linux 系統中,虛擬記憶體機制為每個行程分配了線性連續的記憶體空間,作業系統將這種虛擬記憶體空間映射到物理記憶體空間,每個行程有自己的虛擬記憶體空間,進而不能操作其他行程的記憶體空間,只有作業系統才有權限操作物理記憶體空間, 行程隔離保證了每個行程的記憶體安全,
IPC方式
| 名稱 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Bundle | 簡單易用 | 只能傳輸 Bundle 支持的資料型別 | 四大組件間的行程間通信 |
| 檔案共享 | 簡單易用 | 不適合高并發場景,并且無法做到行程間即時通信 | 無并發訪問情形,交換簡單的資料實時性不高的場景 |
| AIDL | 功能強大,支持一對多并發通信,支持實時通信 | 使用稍復雜,需要處理好執行緒同步 | 一對多通信且有 RPC 需求 |
| Messenger | 功能一般,支持一對多串行通信,支持實時通信 | 不能很處理高并發清醒,不支持 RPC,資料通過 Message 進行傳輸,因此只能傳輸 Bundle 支持的資料型別 | 低并發的一對多即時通信,無RPC需求,或者無需回傳結果的RPC需求 |
| ContentProvider | 在資料源訪問方面功能強大,支持一對多并發資料共享,可通過 Call 方法擴展其他操作 | 可以理解為受約束的 AIDL,主要提供資料源的 CRUD 操作 | 一對多的行程間資料共享 |
| Socket | 可以通過網路傳輸位元組流,支持一對多并發實時通信 | 實作細節稍微有點煩瑣,不支持直接的RPC | 網路資料交換 |
Binder
Binder 是 Android 中的一個類,實作了 IBinder 介面,從 IPC 角度來說,Binder 是 Android 中的一種擴行程通信方方式,從 Android 應用層來說,Binder 是客戶端和服務器端進行通信的媒介,當 bindService 的時候,服務端會回傳一個包含了服務端業務呼叫的 Binder 物件,
Binder 相較于傳統 IPC 來說更適合于Android系統,具體原因的包括如下三點:
- Binder 本身是 C/S 架構的,這一點更符合 Android 系統的架構
- 性能上更有優勢:管道,訊息佇列,Socket 的通訊都需要兩次資料拷貝,而 Binder 只需要一次,要知道,對于系統底層的 IPC 形式,少一次資料拷貝,對整體性能的影響是非常之大的
- 安全性更好:傳統 IPC 形式,無法得到對方的身份標識(UID/GID),而在使用 Binder IPC 時,這些身份標示是跟隨呼叫程序而自動傳遞的,Server 端很容易就可以知道 Client 端的身份,非常便于做安全檢查
示例:
- 新建AIDL介面檔案
RemoteService.aidl
package com.example.mystudyapplication3;
interface IRemoteService {
int getUserId();
}
系統會自動生成 IRemoteService.java:
/*
* This file is auto-generated. DO NOT MODIFY.
*/
package com.example.mystudyapplication3;
// Declare any non-default types here with import statements
//import com.example.mystudyapplication3.IUserBean;
public interface IRemoteService extends android.os.IInterface {
/**
* Local-side IPC implementation stub class.
*/
public static abstract class Stub extends android.os.Binder implements com.example.mystudyapplication3.IRemoteService {
private static final java.lang.String DESCRIPTOR = "com.example.mystudyapplication3.IRemoteService";
/**
* Construct the stub at attach it to the interface.
*/
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.example.mystudyapplication3.IRemoteService interface,
* generating a proxy if needed.
*/
public static com.example.mystudyapplication3.IRemoteService asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.example.mystudyapplication3.IRemoteService))) {
return ((com.example.mystudyapplication3.IRemoteService) iin);
}
return new com.example.mystudyapplication3.IRemoteService.Stub.Proxy(obj);
}
@Override
public android.os.IBinder asBinder() {
return this;
}
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
java.lang.String descriptor = DESCRIPTOR;
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(descriptor);
return true;
}
case TRANSACTION_getUserId: {
data.enforceInterface(descriptor);
int _result = this.getUserId();
reply.writeNoException();
reply.writeInt(_result);
return true;
}
default: {
return super.onTransact(code, data, reply, flags);
}
}
}
private static class Proxy implements com.example.mystudyapplication3.IRemoteService {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
@Override
public int getUserId() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getUserId, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_getUserId = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
}
public int getUserId() throws android.os.RemoteException;
}
| 方法 | 含義 |
|---|---|
| DESCRIPTOR | Binder 的唯一標識,一般用當前的 Binder 的類名表示 |
| asInterface(IBinder obj) | 將服務端的 Binder 物件成客戶端所需的 AIDL 介面型別物件,這種轉換程序是區分行程的,如果位于同一行程,回傳的就是 Stub 物件本身,否則回傳的是系統封裝后的 Stub.proxy 物件, |
| asBinder | 用于回傳當前 Binder 物件 |
| onTransact | 運行在服務端中的 Binder 執行緒池中,遠程請求會通過系統底層封裝后交由此方法來處理 |
| 定向 tag | 含義 |
|---|---|
| in | 資料只能由客戶端流向服務端,服務端將會收到客戶端物件的完整資料,客戶端物件不會因為服務端對傳參的修改而發生變動, |
| out | 資料只能由服務端流向客戶端,服務端將會收到客戶端物件,該物件不為空,但是它里面的欄位為空,但是在服務端對該物件作任何修改之后客戶端的傳參物件都會同步改動, |
| inout | 服務端將會接收到客戶端傳來物件的完整資訊,并且客戶端將會同步服務端對該物件的任何變動, |
流程

AIDL 通信
Android Interface Definition Language
使用示例:
- 新建AIDL介面檔案
// RemoteService.aidl
package com.example.mystudyapplication3;
interface IRemoteService {
int getUserId();
}
- 創建遠程服務
public class RemoteService extends Service {
private int mId = -1;
private Binder binder = new IRemoteService.Stub() {
@Override
public int getUserId() throws RemoteException {
return mId;
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
mId = 1256;
return binder;
}
}
- 宣告遠程服務
<service
android:name=".RemoteService"
android:process=":aidl" />
- 系結遠程服務
public class MainActivity extends AppCompatActivity {
public static final String TAG = "wzq";
IRemoteService iRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iRemoteService = IRemoteService.Stub.asInterface(service);
try {
Log.d(TAG, String.valueOf(iRemoteService.getUserId()));
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
iRemoteService = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindService(new Intent(MainActivity.this, RemoteService.class), mConnection, Context.BIND_AUTO_CREATE);
}
}
Messenger
Messenger可以在不同行程中傳遞 Message 物件,在Message中放入我們需要傳遞的資料,就可以輕松地實作資料的行程間傳遞了,Messenger 是一種輕量級的 IPC 方案,底層實作是 AIDL,
Window / WindowManager
Window 概念與分類
Window 是一個抽象類,它的具體實作是 PhoneWindow,WindowManager 是外界訪問 Window 的入口,Window 的具體實作位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的互動是一個 IPC 程序,Android 中所有的視圖都是通過 Window 來呈現,因此 Window 實際是 View 的直接管理者,
| Window 型別 | 說明 | 層級 |
|---|---|---|
| Application Window | 對應著一個 Activity | 1~99 |
| Sub Window | 不能單獨存在,只能附屬在父 Window 中,如 Dialog 等 | 1000~1999 |
| System Window | 需要權限宣告,如 Toast 和 系統狀態欄等 | 2000~2999 |
Window 的內部機制
Window 是一個抽象的概念,每一個 Window 對應著一個 View 和一個 ViewRootImpl,Window 實際是不存在的,它是以 View 的形式存在,對 Window 的訪問必須通過 WindowManager,WindowManager 的實作類是 WindowManagerImpl:
WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
WindowManagerImpl 沒有直接實作 Window 的三大操作,而是全部交給 WindowManagerGlobal 處理,WindowManagerGlobal 以工廠的形式向外提供自己的實體:
WindowManagerGlobal.java
// 添加
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
···
// 子 Window 的話需要調整一些布局引數
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
···
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// 新建一個 ViewRootImpl,并通過其 setView 來更新界面完成 Window 的添加程序
···
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
// 洗掉
@UnsupportedAppUsage
public void removeView(View view, boolean immediate) {
···
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
···
}
}
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (view != null) {
InputMethodManager imm = InputMethodManager.getInstance();
if (imm != null) {
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
// 更新
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
···
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
在 ViewRootImpl 中最侄訓通過 WindowSession 來完成 Window 的添加、更新、洗掉作業,mWindowSession 的型別是 IWindowSession,是一個 Binder 物件,真正地實作類是 Session,是一個 IPC 程序,
Window 的創建程序
Activity 的 Window 創建程序
在 Activity 的創建程序中,最侄訓由 ActivityThread 的 performLaunchActivity() 來完成整個啟動程序,該方法內部會通過類加載器創建 Activity 的實體物件,并呼叫 attach 方法關聯一系列背景關系環境變數,在 Activity 的 attach 方法里,系統會創建所屬的 Window 物件并設定回呼介面,然后在 Activity 的 setContentView 方法中將視圖附屬在 Window 上:
Activity.java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
···
}
···
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) { // 如果沒有 DecorView,就創建
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 回呼 Activity 的 onContentChanged 方法通知 Activity 視圖已經發生改變
cb.onContentChanged();
}
}
這個時候 DecorView 還沒有被 WindowManager 正式添加,在 ActivityThread 的 handleResumeActivity 方法中,首先會呼叫 Activity 的 onResume 方法,接著呼叫 Activity 的 makeVisible(),完成 DecorView 的添加和顯示程序:
Activity.java
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
Dialog 的 Window 創建程序
Dialog 的 Window 的創建程序和 Activity 類似,創建同樣是通過 PolicyManager 的 makeNewWindow 方法完成的,創建后的物件實際就是 PhoneWindow,當 Dialog 被關閉時,會通過 WindowManager 來移除 DecorView:mWindowManager.removeViewImmediate(mDecor),
Dialog.java
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
···
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
普通 Dialog 必須采用 Activity 的 Context,采用 Application 的 Context 就會報錯,是因為應用 token 所導致,應用 token 一般只有 Activity 擁有,系統 Window 比較特殊,不需要 token,
Toast 的 Window 創建程序
Toast 屬于系統 Window ,由于其具有定時取消功能,所以系統采用了 Handler,Toast 的內部有兩類 IPC 程序,第一類是 Toast 訪問 NotificationManagerService,第二類是 NotificationManagerService 回呼 Toast 里的 TN 介面,
Toast 內部的視圖由兩種方式,一種是系統默認的樣式,另一種是 setView 指定一個自定義 View,它們都對應 Toast 的一個內部成員 mNextView,
Toast.java
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
···
public void cancel() {
mTN.cancel();
}
NotificationManagerService.java
private void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show();
scheduleTimeoutLocked(record, false);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.callback
+ " in package " + record.pkg);
// remove it from the list and let the process die
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
···
private void scheduleTimeoutLocked(ToastRecord r, boolean immediate)
{
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY);
mHandler.removeCallbacksAndMessages(r);
mHandler.sendMessageDelayed(m, delay);
}
Bitmap

配置資訊與壓縮方式
Bitmap 中有兩個內部列舉類:
- Config 是用來設定顏色配置資訊
- CompressFormat 是用來設定壓縮方式
| Config | 單位像素所占位元組數 | 決議 |
|---|---|---|
| Bitmap.Config.ALPHA_8 | 1 | 顏色資訊只由透明度組成,占8位 |
| Bitmap.Config.ARGB_4444 | 2 | 顏色資訊由rgba四部分組成,每個部分都占4位,總共占16位 |
| Bitmap.Config.ARGB_8888 | 4 | 顏色資訊由rgba四部分組成,每個部分都占8位,總共占32位,是Bitmap默認的顏色配置資訊,也是最占空間的一種配置 |
| Bitmap.Config.RGB_565 | 2 | 顏色資訊由rgb三部分組成,R占5位,G占6位,B占5位,總共占16位 |
| RGBA_F16 | 8 | Android 8.0 新增(更豐富的色彩表現HDR) |
| HARDWARE | Special | Android 8.0 新增 (Bitmap直接存盤在graphic memory) |
通常我們優化 Bitmap 時,當需要做性能優化或者防止 OOM,我們通常會使用 Bitmap.Config.RGB_565 這個配置,因為 Bitmap.Config.ALPHA_8 只有透明度,顯示一般圖片沒有意義,Bitmap.Config.ARGB_4444 顯示圖片不清楚, Bitmap.Config.ARGB_8888 占用記憶體最多,
| CompressFormat | 決議 |
|---|---|
| Bitmap.CompressFormat.JPEG | 表示以 JPEG 壓縮演算法進行影像壓縮,壓縮后的格式可以是 .jpg 或者 .jpeg,是一種有損壓縮 |
| Bitmap.CompressFormat.PNG | 顏色資訊由 rgba 四部分組成,每個部分都占 4 位,總共占 16 位 |
| Bitmap.Config.ARGB_8888 | 顏色資訊由 rgba 四部分組成,每個部分都占 8 位,總共占 32 位,是 Bitmap 默認的顏色配置資訊,也是最占空間的一種配置 |
| Bitmap.Config.RGB_565 | 顏色資訊由 rgb 三部分組成,R 占 5 位,G 占 6 位,B 占 5 位,總共占 16 位 |
常用操作
裁剪、縮放、旋轉、移動
Matrix matrix = new Matrix();
// 縮放
matrix.postScale(0.8f, 0.9f);
// 左旋,引數為正則向右旋
matrix.postRotate(-45);
// 平移, 在上一次修改的基礎上進行再次修改 set 每次操作都是最新的 會覆寫上次的操作
matrix.postTranslate(100, 80);
// 裁剪并執行以上操作
Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
雖然Matrix還可以呼叫postSkew方法進行傾斜操作,但是卻不可以在此時創建Bitmap時使用,
Bitmap與Drawable轉換
// Drawable -> Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight();
drawable.draw(canvas);
return bitmap;
}
// Bitmap -> Drawable
public static Drawable bitmapToDrawable(Resources resources, Bitmap bm) {
Drawable drawable = new BitmapDrawable(resources, bm);
return drawable;
}
保存與釋放
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
File file = new File(getFilesDir(),"test.jpg");
if(file.exists()){
file.delete();
}
try {
FileOutputStream outputStream=new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream);
outputStream.flush();
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//釋放bitmap的資源,這是一個不可逆轉的操作
bitmap.recycle();
圖片壓縮
public static Bitmap compressImage(Bitmap image) {
if (image == null) {
return null;
}
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] bytes = baos.toByteArray();
ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
Bitmap bitmap = BitmapFactory.decodeStream(isBm);
return bitmap;
} catch (OutOfMemoryError e) {
e.printStackTrace();
} finally {
try {
if (baos != null) {
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
BitmapFactory
Bitmap創建流程

Option類
| 常用方法 | 說明 |
|---|---|
| boolean inJustDecodeBounds | 如果設定為true,不獲取圖片,不分配記憶體,但會回傳圖片的高度寬度資訊 |
| int inSampleSize | 圖片縮放的倍數 |
| int outWidth | 獲取圖片的寬度值 |
| int outHeight | 獲取圖片的高度值 |
| int inDensity | 用于位圖的像素壓縮比 |
| int inTargetDensity | 用于目標位圖的像素壓縮比(要生成的位圖) |
| byte[] inTempStorage | 創建臨時檔案,將圖片存盤 |
| boolean inScaled | 設定為true時進行圖片壓縮,從inDensity到inTargetDensity |
| boolean inDither | 如果為true,解碼器嘗試抖動解碼 |
| Bitmap.Config inPreferredConfig | 設定解碼器這個值是設定色彩模式,默認值是ARGB_8888,在這個模式下,一個像素點占用4bytes空間,一般對透明度不做要求的話,一般采用RGB_565模式,這個模式下一個像素點占用2bytes |
| String outMimeType | 設定解碼影像 |
| boolean inPurgeable | 當存盤Pixel的記憶體空間在系統記憶體不足時是否可以被回收 |
| boolean inInputShareable | inPurgeable為true情況下才生效,是否可以共享一個InputStream |
| boolean inPreferQualityOverSpeed | 為true則優先保證Bitmap質量其次是解碼速度 |
| boolean inMutable | 配置Bitmap是否可以更改,比如:在Bitmap上隔幾個像素加一條線段 |
| int inScreenDensity | 當前螢屏的像素密度 |
基本使用
try {
FileInputStream fis = new FileInputStream(filePath);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// 設定inJustDecodeBounds為true后,再使用decodeFile()等方法,并不會真正的分配空間,即解碼出來的Bitmap為null,但是可計算出原始圖片的寬度和高度,即options.outWidth和options.outHeight
BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
float srcWidth = options.outWidth;
float srcHeight = options.outHeight;
int inSampleSize = 1;
if (srcHeight > height || srcWidth > width) {
if (srcWidth > srcHeight) {
inSampleSize = Math.round(srcHeight / height);
} else {
inSampleSize = Math.round(srcWidth / width);
}
}
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
} catch (Exception e) {
e.printStackTrace();
}
記憶體回收
if(bitmap != null && !bitmap.isRecycled()){
// 回收并且置為null
bitmap.recycle();
bitmap = null;
}
Bitmap 類的構造方法都是私有的,所以開發者不能直接 new 出一個 Bitmap 物件,只能通過 BitmapFactory 類的各種靜態方法來實體化一個 Bitmap,仔細查看 BitmapFactory 的源代碼可以看到,生成 Bitmap 物件最終都是通過 JNI 呼叫方式實作的,所以,加載 Bitmap 到記憶體里以后,是包含兩部分記憶體區域的,簡單的說,一部分是Java 部分的,一部分是 C 部分的,這個 Bitmap 物件是由 Java 部分分配的,不用的時候系統就會自動回收了,但是那個對應的 C 可用的記憶體區域,虛擬機是不能直接回收的,這個只能呼叫底層的功能釋放,所以需要呼叫 recycle() 方法來釋放 C 部分的記憶體,從 Bitmap 類的源代碼也可以看到,recycle() 方法里也的確是呼叫了 JNI 方法了的,
螢屏適配
單位
- dpi 每英寸像素數(dot per inch)
- dp
密度無關像素 - 一種基于螢屏物理密度的抽象單元, 這些單位相對于 160 dpi 的螢屏,因此一個 dp 是 160 dpi 螢屏上的一個 px, dp 與像素的比率將隨著螢屏密度而變化,但不一定成正比,為不同設備的 UI 元素的實際大小提供了一致性, - sp
與比例無關的像素 - 這與 dp 單位類似,但它也可以通過用戶的字體大小首選項進行縮放,建議在指定字體大小時使用此單位,以便根據螢屏密度和用戶偏好調整它們,
dpi = px / inch
density = dpi / 160
dp = px / density
頭條適配方案
private static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application) {
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (sNoncompatDensity == 0) {
sNoncompatDensity = appDisplayMetrics.density;
sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
// 監聽字體切換
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
// 適配后的dpi將統一為360dpi
final float targetDensity = appDisplayMetrics.widthPixels / 360;
final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
final int targetDensityDpi = (int)(160 * targetDensity);
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi
}
劉海屏適配
- Android P 劉海屏適配方案
Android P 支持最新的全面屏以及為攝像頭和揚聲器預留空間的凹口螢屏,通過全新的 DisplayCutout 類,可以確定非功能區域的位置和形狀,這些區域不應顯示內容,要確定這些凹口螢屏區域是否存在及其位置,使用 getDisplayCutout() 函式,
| DisplayCutout 類方法 | 說明 |
|---|---|
| getBoundingRects() | 回傳Rects的串列,每個Rects都是顯示屏上非功能區域的邊界矩形 |
| getSafeInsetLeft () | 回傳安全區域距離螢屏左邊的距離,單位是px |
| getSafeInsetRight () | 回傳安全區域距離螢屏右邊的距離,單位是px |
| getSafeInsetTop () | 回傳安全區域距離螢屏頂部的距離,單位是px |
| getSafeInsetBottom() | 回傳安全區域距離螢屏底部的距離,單位是px |
Android P 中 WindowManager.LayoutParams 新增了一個布局引數屬性 layoutInDisplayCutoutMode:
| 模式 | 模式說明 |
|---|---|
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有當DisplayCutout完全包含在系統欄中時,才允許視窗延伸到DisplayCutout區域, 否則,視窗布局不與DisplayCutout區域重疊, |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 該視窗決不允許與DisplayCutout區域重疊, |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 該視窗始終允許延伸到螢屏短邊上的DisplayCutout區域, |
- Android P 之前的劉海屏適配
不同廠商的劉海屏適配方案不盡相同,需分別查閱各自的開發者檔案,
Context
Context 本身是一個抽象類,是對一系列系統服務介面的封裝,包括:內部資源、包、類加載、I/O操作、權限、主執行緒、IPC 和組件啟動等操作的管理,ContextImpl, Activity, Service, Application 這些都是 Context 的直接或間接子類, 關系如下:

ContextWrapper是代理Context的實作,簡單地將其所有呼叫委托給另一個Context(mBase),
Application、Activity、Service通過attach() 呼叫父類ContextWrapper的attachBaseContext(), 從而設定父類成員變數 mBase 為 ContextImpl 物件, ContextWrapper 的核心作業都是交給 mBase(ContextImpl) 來完成,這樣可以子類化 Context 以修改行為而無需更改原始 Context,
SharedPreferences
SharedPreferences 采用key-value(鍵值對)形式, 主要用于輕量級的資料存盤, 尤其適合保存應用的配置引數, 但不建議使用 SharedPreferences 來存盤大規模的資料, 可能會降低性能.
SharedPreferences采用xml檔案格式來保存資料, 該檔案所在目錄位于 /data/data/<package name>/shared_prefs,如:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="blog">https://github.com/JasonWu1111/Android-Review</string>
</map>
從Android N開始, 創建的 SP 檔案模式, 不允許 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 模塊, 否則會直接拋出例外 SecurityException, MODE_MULTI_PROCESS 這種多行程的方式也是 Google 不推薦的方式, 后續同樣會不再支持,
當設定 MODE_MULTI_PROCESS 模式, 則每次 getSharedPreferences 程序, 會檢查 SP 檔案上次修改時間和檔案大小, 一旦所有修改則會重新從磁盤加載檔案,
獲取方式
getPreferences
Activity.getPreferences(mode): 以當前 Activity 的類名作為 SP 的檔案名. 即 xxxActivity.xml Activity.java
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
getDefaultSharedPreferences
PreferenceManager.getDefaultSharedPreferences(Context): 以包名加上 _preferences 作為檔案名, 以 MODE_PRIVATE 模式創建 SP 檔案. 即 packgeName_preferences.xml.
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
getSharedPreferences
直接呼叫 Context.getSharedPreferences(name, mode),所有的方法最終都是呼叫到如下方法:
class ContextImpl extends Context {
private ArrayMap<String, File> mSharedPrefsPaths;
public SharedPreferences getSharedPreferences(String name, int mode) {
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//先從mSharedPrefsPaths查詢是否存在相應檔案
file = mSharedPrefsPaths.get(name);
if (file == null) {
//如果檔案不存在, 則創建新的檔案
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
}
架構

SharedPreferences 與 Editor 只是兩個介面. SharedPreferencesImpl 和 EditorImpl 分別實作了對應介面,另外, ContextImpl 記錄著 SharedPreferences 的重要資料,
putxxx() 操作把資料寫入到EditorImpl.mModified;
apply()/commit() 操作先呼叫 commitToMemory(), 將資料同步到 SharedPreferencesImpl 的 mMap, 并保存到 MemoryCommitResult 的 mapToWriteToDisk,再呼叫 enqueueDiskWrite(), 寫入到磁盤檔案; 先之前把原有資料保存到 .bak 為后綴的檔案,用于在寫磁盤的程序出現任何例外可恢復資料;
getxxx() 操作從 SharedPreferencesImpl.mMap 讀取資料.
apply / commit
- apply 沒有回傳值, commit 有回傳值能知道修改是否提交成功
- apply 是將修改提交到記憶體,再異步提交到磁盤檔案,而 commit 是同步的提交到磁盤檔案
- 多并發的提交 commit 時,需等待正在處理的 commit 資料更新到磁盤檔案后才會繼續往下執行,從而降低效率; 而 apply 只是原子更新到記憶體,后呼叫 apply 函式會直接覆寫前面記憶體資料,從一定程度上提高很多效率,
注意
- 強烈建議不要在 sp 里面存盤特別大的 key/value,有助于減少卡頓 / anr
- 不要高頻地使用 apply,盡可能地批量提交
- 不要使用 MODE_MULTI_PROCESS
- 高頻寫操作的 key 與高頻讀操作的 key 可以適當地拆分檔案,由于減少同步鎖競爭
- 不要連續多次 edit(),應該獲取一次獲取 edit(),然后多次執行 putxxx(),減少記憶體波動
訊息機制
Handler 機制
Handler 有兩個主要用途:(1)安排 Message 和 runnables 在將來的某個時刻執行; (2)將要在不同于自己的執行緒上執行的操作排入佇列,(在多個執行緒并發更新UI的同時保證執行緒安全,)
Android 規定訪問 UI 只能在主執行緒中進行,因為 Android 的 UI 控制元件不是執行緒安全的,多執行緒并發訪問會導致 UI 控制元件處于不可預期的狀態,為什么系統不對 UI 控制元件的訪問加上鎖機制?缺點有兩個:加鎖會讓 UI 訪問的邏輯變得復雜;其次鎖機制會降低 UI 訪問的效率,如果子執行緒訪問 UI,那么程式就會拋出例外,ViewRootImpl 對UI操作做了驗證,這個驗證作業是由 ViewRootImpl的 checkThread 方法完成:
ViewRootImpl.java
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
- Message:Handler 接收和處理的訊息物件
- MessageQueue:Message 的佇列,先進先出,每一個執行緒最多可以擁有一個
- Looper:訊息泵,是 MessageQueue 的管理者,會不斷從 MessageQueue 中取出訊息,并將訊息分給對應的 Handler 處理,每個執行緒只有一個 Looper,
Handler 創建的時候會采用當前執行緒的 Looper 來構造訊息回圈系統,需要注意的是,執行緒默認是沒有 Looper 的,直接使用 Handler 會報錯,如果需要使用 Handler 就必須為執行緒創建 Looper,因為默認的 UI 主執行緒,也就是 ActivityThread,ActivityThread 被創建的時候就會初始化 Looper,這也是在主執行緒中默認可以使用 Handler 的原因,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/328037.html
標籤:其他
