一、使用CrashHandler來獲取Crash信息
通過設置Thread. setDefaultUncaughtExceptionHandler;
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {}
二、使用Multidex來解決方法數越界
compile 'com.android.support:multidex:1.0.2'
public class MyApplication extends MultiDexApplication { ... }
三、Android Apk編譯打包流程

編譯打包步驟:
1. 打包資源文件,生成R.java文件
打包資源的工具是aapt(The Android Asset Packaing Tool(Android\sdk\build-tools\25.0.0\aapt.exe)。在這個過程中,項目中的AndroidManifest.xml文件和布局文件XML都會編譯,然后生成相應的R.java,另外AndroidManifest.xml會被aapt編譯成二進制。
存放在APP的res目錄下的資源,該類資源在APP打包前大多會被編譯,變成二進制文件,并會為每個該類文件賦予一個resource id。對于該類資源的訪問,應用層代碼則是通過resource id進行訪問的。Android應用在編譯過程中aapt工具會對資源文件進行編譯,并生成一個resource.arsc文件,resource.arsc文件相當于一個文件索引表,記錄了很多跟資源相關的信息。
2. 處理aidl文件,生成相應的Java文件
這一過程中使用到的工具是aidl(Android Interface Definition Language),即Android接口描述語言(Android\sdk\build-tools\25.0.0\aidl.exe)。
aidl工具解析接口定義文件然后生成相應的Java代碼接口供程序調用。如果在項目沒有使用到aidl文件,則可以跳過這一步。
3. 編譯項目源代碼,生成class文件
項目中所有的Java代碼,包括R.java和.aidl文件,都會變Java編譯器(javac)編譯成.class文件,生成的class文件位于工程中的bin/classes目錄下。
4. 轉換所有的class文件,生成classes.dex文件
dx工具生成可供Android系統(tǒng)Dalvik虛擬機執(zhí)行的classes.dex文件,該工具位于(Android\sdk\build-tools\25.0.0\dx.bat)。
任何第三方的libraries和.class文件都會被轉換成.dex文件。dx工具的主要工作是將Java字節(jié)碼轉成成Dalvik字節(jié)碼、壓縮常量池、消除冗余信息等。
5. 打包生成APK文件
所有沒有編譯的資源,如images、assets目錄下資源(該類文件是一些原始文件,APP打包時并不會對其進行編譯,而是直接打包到APP中,對于這一類資源文件的訪問,應用層代碼需要通過文件名對其進行訪問);編譯過的資源和.dex文件都會被apkbuilder工具打包到最終的.apk文件中。
打包的工具apkbuilder位于 android-sdk/tools目錄下。apkbuilder為一個腳本文件,實際調用的是(E:\Documents\Android\sdk\tools\lib)文件中的com.android.sdklib.build.ApkbuilderMain類。
6. 對APK文件進行簽名
一旦APK文件生成,它必須被簽名才能被安裝在設備上。
在開發(fā)過程中,主要用到的就是兩種簽名的keystore。一種是用于調試的debug.keystore,它主要用于調試,在Eclipse或者Android Studio中直接run以后跑在手機上的就是使用的debug.keystore。
另一種就是用于發(fā)布正式版本的keystore。
7. 對簽名后的APK文件進行對齊處理
如果你發(fā)布的apk是正式版的話,就必須對APK進行對齊處理,用到的工具是zipalign(E:\Documents\Android\sdk\build-tools\25.0.0\zipalign.exe)
對齊的主要過程是將APK包中所有的資源文件距離文件起始偏移為4字節(jié)整數倍,這樣通過內存映射訪問apk文件時的速度會更快。對齊的作用就是減少運行時內存的使用。
四、Android Apk安裝過程
Apk安裝的主要步驟:
- 將apk文件復制到data/app目錄
- 解析apk信息
- Dalvik虛擬機執(zhí)行dexopt操作(優(yōu)化.dex文件成odex文件到data/dalvik-cache);如果是ART虛擬機執(zhí)行dex2oat操作(將.dex文件翻譯成.oat文件)
- 更新權限信息
- 完成安裝,發(fā)送Intent.ACTION_PACKAGE_ADDED廣播

五、Android的動態(tài)加載技術
動態(tài)加載技術也叫插件化技術,通過插件化來減輕應用的內存和CPU占用。
主要解決三個基礎性問題:
1、資源訪問
ContextImpl中有兩個抽象方法:getAssets和getResources方法
2、Activity生命周期管理
將Activity生命周期方法提取出來作為一個接口,通過代理Activity去調生命周期方法。
3、ClassLoader的管理
同一個插件采用同一個ClassLoader加載類。
六、Android權限機制
Android將安全設計貫穿系統(tǒng)架構的各個層面,覆蓋系統(tǒng)內核、虛擬機、應用程序框架層以及應用層各個環(huán)節(jié),力求在開放的同時,也最大程度地保護用戶的數據、應用程序和設備的安全。Android安全模型主要提供以下幾種安全機制:

1、權限的本質
在 Android 中,一個權限,本質上是一個字符串,一個可以表示執(zhí)行特定操作的能力的字符串。訪問 SD 卡的能力,訪問通訊錄的能力,啟動或訪問一個第三方應用中的組件的能力。
pm list permissions -f 命令可以詳細查看 Android 所有預定義的權限
權限的信息包括:定義的包名、標簽、描述和保護級別
+ permission:android.permission.DELETE_PACKAGES
package:android
label:null
description:null
protectionLevel:signature|privileged
2、權限的級別
normal 級別:
權限保護級別的默認值,無須用戶確認,只要聲明了,就自動默默授權。如:ACCESS_NETWORK_STATE。dangerous 級別:
賦予權限前,會彈出對話框,顯式請求權限。如:READ_SMS。因為 Android 需要在安裝時賦予權限,所以安裝的確認對話框,也會顯示列出權限清單。signature 級別:
signature 級別的權限是最嚴格的權限,只會賦予與聲明權限使用相同證書的應用程序。
以系統(tǒng)內置 signature 級別權限為例,Android 系統(tǒng)應用的簽名由平臺密鑰簽發(fā),默認情況下源碼樹里有 4 個不同的密鑰文件:platform、shared、media 和 testkey。所有核心平臺的包(如:設置、電話、藍牙)均使用 platform 密鑰簽發(fā);搜索和通訊錄相關的包使用 shared 簽發(fā);圖庫和媒體相關的包使用 media 密鑰簽發(fā);其他的應用使用 testkey 簽發(fā)。定義系統(tǒng)內置權限的 framework-res.apk 文件是使用平臺密鑰簽發(fā)的,因此任何試圖請求 signature 級別內置權限的應用程序,需要使用與框架資源包相同的密鑰進行簽名。
- signatureOrSystem 級別:
可以看做是一種折中的級別,可被賦予與聲明權限具有相同簽名證書密鑰的應用程序(同 signature 級別)或者系統(tǒng)鏡像的部分應用,也就是說這允許廠商無須共享簽名密鑰。Android 4.3 之前,安裝在 system 分區(qū)下的應用會被自動賦予該保護級別的權限,而 Android 4.4 之后,只允許安裝在 system/priv-app/ 目錄下的應用才能被主動賦予。
3、權限的管理
在每個應用安裝時,權限就已經賦予了,系統(tǒng)使用包管理服務來管理權限。打開我們系統(tǒng)目錄下的 /data/system/packages.xml,可以看到文件包含了所有已定義的權限列表和所有 apk 的包信息,這可以看做是包管理服務維護的一個已安裝程序的核心數據庫,這個數據庫,隨著每次應用安裝、升級或卸載而進行更新。
注意:
6.0以下權限在/data/system/packages.xml
6.0以上權限在/data/system/users/0/runtime-permissions.xml
4、權限的賦予
我們知道,Android 應用安裝時,會被分配一個唯一的 UID,應用啟動時,包管理器會設置新建進程的 UID 和 GID 為應用程序的 UID。如果應用已經被賦予了額外的權限,就把這些權限映射成一組 GID,作為補充 GID 分配給進程。低層就可以依賴于進程的 UID、GID 和補充 GID 來決定是否賦予權限了。
內置權限到 GID 的映射是定義在 /etc/permission/platform.xml ;
6.0以上動態(tài)添加到以上位置
系統(tǒng)進程的權限配置信息在
Android\system\core\include\private\android_filesystem_config.h
注意:
PID:表示應用的進程 ID,PPID 表示父進程 ID;
UID:UID代表一個應用;應用安裝時分配一個唯一的;
5、權限的檢查
1. 系統(tǒng)內核層權限檢查
內核代碼中current_has_network(void) 方法檢查了進程的所在組。如果不在 inet 組,則直接返回錯誤。設置申請權限,經過解析,逐步映射到內核層的組 ID 和用戶 ID,最終才能通過內核層的檢查。
2. 框架層權限檢查
Android 6.0 之前組件不能在運行時改變權限,所以系統(tǒng)的權限檢查執(zhí)行過程是靜態(tài)的
- 動態(tài)權限執(zhí)行:
通過IPC:Android 的核心系統(tǒng)服務統(tǒng)一會注冊到服務管理器,系統(tǒng)服務可以直接檢查調用者的 UID,通過限定 UID 來控制訪問權限;不適合非固定UID的應用,適合只允許以 root(UID:0) 或 system(UID:1000) 運行的進程訪問的服務檢查。
- 靜態(tài)權限執(zhí)行
跨應用組件交互
我們使用隱式 Intent 來表達意圖,搜索匹配的組件,如果有多個,彈出選擇框,目標組件被選定后,會由 ActivityManagerService 執(zhí)行權限檢查,檢查目標組件是否有相應的權限要求,如果有,則把權限檢查的工作交給 PMS,去檢查調用者有沒有被授權這些權限。
接下來的總體的流程和動態(tài)執(zhí)行流程大致相同:Binder.getCallingUid()和Binder.getCallingPid()獲取調用者的 UID 和 PID,然后利用 UID 映射包名,再獲得相關權限集合。如果權限集合中含有所需權限即啟動,否則拋出 SecurityException 異常。靜態(tài)權限執(zhí)行這里,我們可以詳細了解下,每種組件的權限檢查時機和具體順序是怎么樣的。
- 組件權限執(zhí)行
Activity
會在 startActivity() 和 startActivityForResult() 里解析到聲明權限的 Activity 時,就執(zhí)行權限檢查。
Service
startService()、stopService() 和 bindService(),這 3 個方法被調用時都會進行權限檢查。
BroadCastReceiver
發(fā)送廣播除了常用的 sendBroadcast(Intent intent),還有個 sendBroadcast(Intent intent, String receiverPermission),該方法可以要求廣播接受者具備特定的權限,但是,調用 sendBroadcast 是不會進行權限檢查的,因為廣播是異步的,所以權限檢查會在 intent 傳遞到已注冊的廣播接受者時進行,如果接收者不具備特定的權限,則不會接收到該廣播,也不會收到 SecurityException 異常。
反過來,接收者可以要求廣播發(fā)送者必須具備的權限,所要求的權限在 manifest 文件中設置 <receiver> 標簽的 permission 屬性,或者動態(tài)注冊時指定 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler),權限檢查也是在廣播傳遞時執(zhí)行。
所以,收發(fā)廣播可以分開指定權限。值得一提的是,一些系統(tǒng)廣播被聲明為 protected,并且只能由系統(tǒng)進程發(fā)送,比如 PACKAGE_INSTALLED。只能由系統(tǒng)進程發(fā)送,這個限制會在內核層進行檢查,對調用者的 UID 進行匹配,只能是 SYSTEM_UID、PHONE_UID、SHELL_UID、BLUETOOTH_UID 或 root。如果其他 UID 的進程試圖發(fā)送系統(tǒng)廣播,則會收到 SecurityException 異常。
ContentProvider
ContentProvider 可以為讀寫分別指定不同的權限,即:調用目標 provider、query() 方法 和 insert()、update()、delete() 都會進行權限檢查。
總結:
Android 的權限的檢查會在各個層次上實施。
1、高層的組件,例如應用和系統(tǒng)服務,通過包管理器查詢應用程序被賦予的權限,并決定是否準予訪問。
2、低層的組件,通常不訪問包管理器,比如本地守護進程,依賴于進程的 UID、GID 和補充 GID 來決定賦予。
3、訪問系統(tǒng)資源時,如設備文件、UNIX 域套接字和網絡套接字,則由內核根據所有者、目標資源的訪問權限和訪問進程的進程屬性或者 packages.list 來進行控制。
共享 UID
最后簡單說下共享 UID,填一下前面挖的坑。雖說 Android 會為每一個應用分配唯一的 UID,但如果應用使用相同的密鑰簽發(fā),就可以使用相同 UID 運行,也就是運行在同一個進程中。
這個特性被系統(tǒng)應用和核心框架服務廣泛使用,比如:Google Play 和 Google 定位服務,請求同一進程內的 Google 登錄服務,從而達到靜默自動同步用戶數據的體驗。
值得注意的是:Android 不支持將一個已安裝的應用,從非共享 UID 切換到共享狀態(tài),因為改變了已安裝應用的 UID,會導致應用失去對自己文件的訪問權限(在一些早期 Android 版本中),所以如果使用共享 UID 必須從一開始就設計好。
七、Android劉海屏適配
1、AndroidP劉海屏的適配:
Android P 支持最新的全面屏以及為攝像頭和揚聲器預留空間的凹口屏幕。通過全新的 DisplayCutout 類,可以確定非功能區(qū)域的位置和形狀,這些區(qū)域不應顯示內容。要確定這些凹口屏幕區(qū)域是否存在及其位置,使用 getDisplayCutout() 函數。
- 設置LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式
- 設置沉浸式布局模式
- 計算狀態(tài)欄高度,進行布局;如果有特殊UI要求,則可以使用DisplayCutoutDemo類去獲取劉海屏的坐標,完成UIAndroid P 中 WindowManager.LayoutParams 新增了一個布局參數屬性layoutInDisplayCutoutMode:
| DisplayCutout 類方法 | 說明 |
|---|---|
| getBoundingRects() | 返回Rects的列表,每個Rects都是顯示屏上非功能區(qū)域的邊界矩形 |
| getSafeInsetLeft () | 返回安全區(qū)域距離屏幕左邊的距離,單位是px |
| getSafeInsetRight () | 返回安全區(qū)域距離屏幕右邊的距離,單位是px |
| getSafeInsetTop () | 返回安全區(qū)域距離屏幕頂部的距離,單位是px |
| getSafeInsetBottom() | 返回安全區(qū)域距離屏幕底部的距離,單位是px |
View decorView = mAc.getWindow().getDecorView();
if(decorView != null){
Log.d("hwj", "**controlView**" + android.os.Build.VERSION.SDK_INT);
Log.d("hwj", "**controlView**" + android.os.Build.VERSION_CODES.P);
WindowInsets windowInsets = decorView.getRootWindowInsets();
if(windowInsets != null){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
DisplayCutout displayCutout = windowInsets.getDisplayCutout();
//getBoundingRects返回List<Rect>,沒一個list表示一個不可顯示的區(qū)域,即劉海屏,可以遍歷這個list中的Rect,
//即可以獲得每一個劉海屏的坐標位置,當然你也可以用類似getSafeInsetBottom的api
Log.d("hwj", "**controlView**" + displayCutout.getBoundingRects());
Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetBottom());
Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetLeft());
Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetRight());
Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetTop());
}
}
}
Android P 中 WindowManager.LayoutParams 新增了一個布局參數屬性 layoutInDisplayCutoutMode:
| 模式 | 模式說明 |
|---|---|
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有當DisplayCutout完全包含在系統(tǒng)欄中時,才允許窗口延伸到DisplayCutout區(qū)域。 否則,窗口布局不與DisplayCutout區(qū)域重疊。 |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 該窗口決不允許與DisplayCutout區(qū)域重疊。 |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 該窗口始終允許延伸到屏幕短邊上的DisplayCutout區(qū)域。Android P 之前的劉海屏適配 |
2、AndroidP之前劉海屏的適配
不同廠商的劉海屏適配方案不盡相同,需分別查閱各自的開發(fā)者文檔。
3、劉海屏判斷
/**
* 判斷是否是劉海屏
* @return
*/
public static boolean hasNotchScreen(Activity activity){
if (getInt("ro.miui.notch",activity) == 1 || hasNotchAtHuawei(activity) || hasNotchAtOPPO()
|| hasNotchAtVivo(activity) || isAndroidP(activity) != null){ //TODO 各種品牌
return true;
}
return false;
}
/**
* Android P 劉海屏判斷
* @param activity
* @return
*/
public static DisplayCutout isAndroidP(Activity activity){
View decorView = activity.getWindow().getDecorView();
if (decorView != null && android.os.Build.VERSION.SDK_INT >= 28){
WindowInsets windowInsets = decorView.getRootWindowInsets();
if (windowInsets != null)
return windowInsets.getDisplayCutout();
}
return null;
}
/**
* 小米劉海屏判斷.
* @return 0 if it is not notch ; return 1 means notch
* @throws IllegalArgumentException if the key exceeds 32 characters
*/
public static int getInt(String key,Activity activity) {
int result = 0;
if (isXiaomi()){
try {
ClassLoader classLoader = activity.getClassLoader();
@SuppressWarnings("rawtypes")
Class SystemProperties = classLoader.loadClass("android.os.SystemProperties");
//參數類型
@SuppressWarnings("rawtypes")
Class[] paramTypes = new Class[2];
paramTypes[0] = String.class;
paramTypes[1] = int.class;
Method getInt = SystemProperties.getMethod("getInt", paramTypes);
//參數
Object[] params = new Object[2];
params[0] = new String(key);
params[1] = new Integer(0);
result = (Integer) getInt.invoke(SystemProperties, params);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 華為劉海屏判斷
* @return
*/
public static boolean hasNotchAtHuawei(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class HwNotchSizeUtil = classLoader.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
AppLog.e("hasNotchAtHuawei ClassNotFoundException");
} catch (NoSuchMethodException e) {
AppLog.e("hasNotchAtHuawei NoSuchMethodException");
} catch (Exception e) {
AppLog.e( "hasNotchAtHuawei Exception");
} finally {
return ret;
}
}
public static final int VIVO_NOTCH = 0x00000020;//是否有劉海
public static final int VIVO_FILLET = 0x00000008;//是否有圓角
/**
* VIVO劉海屏判斷
* @return
*/
public static boolean hasNotchAtVivo(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class FtFeature = classLoader.loadClass("android.util.FtFeature");
Method method = FtFeature.getMethod("isFeatureSupport", int.class);
ret = (boolean) method.invoke(FtFeature, VIVO_NOTCH);
} catch (ClassNotFoundException e) {
AppLog.e( "hasNotchAtVivo ClassNotFoundException");
} catch (NoSuchMethodException e) {
AppLog.e( "hasNotchAtVivo NoSuchMethodException");
} catch (Exception e) {
AppLog.e( "hasNotchAtVivo Exception");
} finally {
return ret;
}
}
/**
* OPPO劉海屏判斷
* @return
*/
public static boolean hasNotchAtOPPO(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}