天天看點

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程序Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

本次的學習指南系列将分為三個子產品

  • 第一 Java知識點彙總
  • 第二 Android基礎知識點彙總(一)(二)
  • 第三 Android進階知識點彙總

後續的NDK、跨平台、底層源碼等技術,也會抽空給大家整理一下知識點,如果喜歡的話,希望大家給個關注,點個贊呗,個人首頁簡介有聯系方式,可找我拿PDF版本的學習指南。

怎麼聯系我:Android.md · master · 讓開,我要吃人了 / Android · CODE CHINACODE CHINA——開源代碼托管平台,獨立第三方開源社群,Git/Github/Gitlab

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

https://codechina.csdn.net/weixin_55362248/android/-/blob/master/Android.md

Activity

生命周期

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制
  • 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 中打開

啟動過程

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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()。

生命周期

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制
Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

與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 的互動。

啟動過程

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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();
        }
    } 
    ··· 
}
           

綁定過程

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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();
            }
        } 
        ···
    }
}
           

生命周期

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制
說明
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,需要的話則重繪制
Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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 前台程序(Foreground process)
PERSISTENT_SERVICE_ADJ -11 關聯着系統或persistent程序
PERSISTENT_PROC_ADJ -12 系統 persistent 程序,比如telephony
SYSTEM_ADJ -16 系統程序
NATIVE_ADJ -17 native程序(不被系統管理)

程序被殺情況

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

程序保活方案

  • 開啟一個像素的 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 服務端将會接收到用戶端傳來對象的完整資訊,并且用戶端将會同步服務端對該對象的任何變動。

流程

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

配置資訊與壓縮方式

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建立流程

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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 的直接或間接子類, 關系如下:

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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);
    }
}
           

架構

Android學習指南 — Android基礎知識彙總ActivityFragmentServiceBroadcastReceiverContentProvider資料存儲View程式Parcelable 接口IPCWindow / WindowManagerBitmap螢幕适配ContextSharedPreferences消息機制

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 的原因。