Android複習資料——Android知識點彙總(一)

接觸 Android 開發也有一段時間了,前段時間便開始想抽空整理一些知識點,經過筆記整理的方式減小本身重複學習的時間成本和提升自身的效率。php

目前先是總結了部分 Android 的知識點,這就是本文的主要分享內容。想特地申明的一點是,這個總結更多的是從本人本身的編程基礎和側重點出發,因此在內容上會有選擇性的忽略以及側重點,參考的博客和圖文有不少,沒辦法一一列出,若是有引用不當的部分會當即刪除,望你們見諒。 知識點目錄能夠再右邊側邊欄查看跳轉。java

另外,以後會整理的知識點還會有 java、Android 源碼、其餘的一些計算機基礎以及常見的面試題等幾個部分,日後的一個月時間裏會陸續補充更新,在 Github 上建立了項目,想關注的歡迎 starandroid

進程和線程

當某個應用組件啓動且該應用沒有運行其餘任何組件時,Android 系統會使用單個執行線程爲應用啓動新的 Linux 進程。默認狀況下,同一應用的全部組件在相同的進程和線程(稱爲「主」線程)中運行。git

各種組件元素的清單文件條目<activity><service><receiver><provider>—均支持 android:process 屬性,此屬性能夠指定該組件應在哪一個進程運行。github

進程生命週期

一、前臺進程面試

  • 託管用戶正在交互的 Activity(已調用 Activity 的 onResume() 方法)
  • 託管某個 Service,後者綁定到用戶正在交互的 Activity
  • 託管正在「前臺」運行的 Service(服務已調用 startForeground()
  • 託管正執行一個生命週期回調的 Service(onCreate()onStart()onDestroy()
  • 託管正執行其 onReceive() 方法的 BroadcastReceiver

二、可見進程算法

  • 託管不在前臺、但仍對用戶可見的 Activity(已調用其 onPause() 方法)。例如,若是 re前臺 Activity 啓動了一個對話框,容許在其後顯示上一 Activity,則有可能會發生這種狀況。
  • 託管綁定到可見(或前臺)Activity 的 Service

三、服務進程數據庫

  • 正在運行已使用 startService() 方法啓動的服務且不屬於上述兩個更高類別進程的進程。

四、後臺進程編程

  • 包含目前對用戶不可見的 Activity 的進程(已調用 Activity 的 onStop() 方法)。一般會有不少後臺進程在運行,所以它們會保存在 LRU (最近最少使用)列表中,以確保包含用戶最近查看的 Activity 的進程最後一個被終止。

五、空進程canvas

  • 不含任何活動應用組件的進程。保留這種進程的的惟一目的是用做緩存,以縮短下次在其中運行組件所需的啓動時間。 爲使整體系統資源在進程緩存和底層內核緩存之間保持平衡,系統每每會終止這些進程。\

多進程

若是註冊的四大組件中的任意一個組件時用到了多進程,運行該組件時,都會建立一個新的 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;
    }
}
複製代碼

進程存活

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喚醒
  • 粘性服務&與系統服務捆綁

線程

應用啓動時,系統會爲應用建立一個名爲「主線程」的執行線程( UI 線程)。 此線程很是重要,由於它負責將事件分派給相應的用戶界面小部件,其中包括繪圖事件。 此外,它也是應用與 Android UI 工具包組件(來自 android.widgetandroid.view 軟件包的組件)進行交互的線程。

系統不會爲每一個組件實例建立單獨的線程。運行於同一進程的全部組件均在 UI 線程中實例化,而且對每一個組件的系統調用均由該線程進行分派。 所以,響應系統回調的方法(例如,報告用戶操做的 onKeyDown() 或生命週期回調方法)始終在進程的 UI 線程中運行。

Android 的單線程模式必須遵照兩條規則:

  • 不要阻塞 UI 線程
  • 不要在 UI 線程以外訪問 Android UI 工具包

爲解決此問題,Android 提供了幾種途徑來從其餘線程訪問 UI 線程:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

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 網絡數據交換

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。

Context

Context自己是一個抽象類,是對一系列系統服務接口的封裝,包括:內部資源、包、類加載、I/O操做、權限、主線程、IPC和組件啓動等操做的管理。ContextImpl, Activity, Service, Application這些都是Context的直接或間接子類, 關係以下:

ContextWrapper是代理Context的實現,簡單地將其全部調用委託給另外一個Context(mBase)。

Application、Activity、Service經過attach()調用父類ContextWrapper的attachBaseContext(), 從而設置父類成員變量mBase爲ContextImpl對象;, ontextWrapper的核心工做都是交給mBase(即ContextImpl)來完成.

Activity

生命週期

  • Activity A 啓動另外一個Activity B,回調以下:
    Activity A 的onPause() → Activity B的onCreate() → onStart() → onResume() → Activity A的onStop();若是B是透明主題又或則是個DialogActivity,則不會回調A的onStop;

啓動模式

LaunchMode 說明
standard 系統在啓動它的任務中建立activity的新實例
singleTop 若是activity的實例已存在於當前任務的頂部,則系統經過調用其onNewIntent()
singleTask 系統建立新task並在task的根目錄下實例化activity。但若是activity的實例已存在於單獨的任務中,則調用其onNewIntent()方法。一次只能存在一個activity實例
singleInstance 相同"singleTask",activity始終是其task的惟一成員; 任何由此開始的activity都在一個單獨的task中打開

 

使用Intent標誌 說明
FLAG_ACTIVITY_NEW_TASK 同singleTask
FLAG_ACTIVITY_SINGLE_TOP 同singleTop
FLAG_ACTIVITY_CLEAR_TOP 若是正在啓動的activity已在當前task中運行,則不會啓動該activity的新實例,而是銷燬其上的activity,並調用其onNewIntent()

啓動過程

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對象 [見小節4.1]
        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

生命週期

說明
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);
複製代碼

數據存儲

存儲方式 說明
SharedPreferences 在鍵值對中存儲私有原始數據
內部存儲 在設備內存中存儲私有數據
外部存儲 在共享的外部存儲中存儲公共數據
SQLite 數據庫 在私有數據庫中存儲結構化數據

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_READABLEMODE_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(),減小內存波動

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共同決定

直接繼承View的控件須要重寫onMeasure方法並設置wrap_content時的自身大小,不然在佈局中使用wrap_content就至關於使用match_parent。解決方式以下:

protected void onMeasure(int widthMeasureSpec, int height MeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widtuhSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    // 在wrap_content的狀況下指定內部寬/高(mWidth和mHeight)
    int heightSpecSize = 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);
    }
}
複製代碼

在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)
}
複製代碼

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方法了的。

Handler

Handler有兩個主要用途:(1)安排Message和runnables在未來的某個時刻執行; (2)將要在不一樣於本身的線程上執行的操做排入隊列。(在多個線程併發更新UI的同時保證線程安全。)

Handler建立的時候會採用當前線程的Looper來構造消息循環系統,須要注意的是,線程默認是沒有Looper的,若是須要使用Handler就必須爲線程建立Looper,由於默認的UI主線程,也就是ActivityThread,ActivityThread被建立的時候就會初始化Looper,這也是在主線程中默承認以使用Handler的緣由。

  • Message:Handler接收和處理的消息對象
  • MessageQueue:Message的隊列,先進先出,每個線程最多能夠擁有一個
  • Looper:消息泵,是MessageQueue的管理者,會不斷從MessageQueue中取出消息,並將消息分給對應的Handler處理,每一個線程只有一個Looper。

AsyncTask

  • 異步任務的實例必須在UI線程中建立,即AsyncTask對象必須在UI線程中建立。
  • execute(Params... params)方法必須在UI線程中調用。
  • 不要手動調用onPreExecute(),doInBackground(),onProgressUpdate(),onPostExecute()這幾個方法。
  • 不能在doInBackground()中更改UI組件的信息。
  • 一個任務實例只能執行一次,若是執行第二次將會拋出異常。
import android.os.AsyncTask;

public class DownloadTask extends AsyncTask<String, Integer, Boolean> {

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    @Override
    protected Boolean doInBackground(String... strings) {
        return null;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
    }
}
複製代碼

Serializable/Parcelable

  • Serializable 使用 I/O 讀寫存儲在硬盤上,而 Parcelable 是直接 在內存中讀寫
  • Serializable 會使用反射,序列化和反序列化過程須要大量 I/O 操做, Parcelable 自已實現封送和解封(marshalled &unmarshalled)操做不須要用反射,數據也存放在 Native 內存中,效率要快不少

屏幕適配

單位

  • 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
}
複製代碼

本次的分享就到這啦,喜歡的話能夠點個贊👍或關注。若有錯誤的地方歡迎你們在評論裏指出。

本文爲我的原創,轉載請註明出處。

相關文章
相關標籤/搜索