Android開發(fā)十五《綜合技術》

一、使用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安裝的主要步驟:

  1. 將apk文件復制到data/app目錄
  2. 解析apk信息
  3. Dalvik虛擬機執(zhí)行dexopt操作(優(yōu)化.dex文件成odex文件到data/dalvik-cache);如果是ART虛擬機執(zhí)行dex2oat操作(將.dex文件翻譯成.oat文件)
  4. 更新權限信息
  5. 完成安裝,發(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權限機制嗎

七、Android劉海屏適配

1、AndroidP劉海屏的適配:

Android P 支持最新的全面屏以及為攝像頭和揚聲器預留空間的凹口屏幕。通過全新的 DisplayCutout 類,可以確定非功能區(qū)域的位置和形狀,這些區(qū)域不應顯示內容。要確定這些凹口屏幕區(qū)域是否存在及其位置,使用 getDisplayCutout() 函數。

  1. 設置LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式
  2. 設置沉浸式布局模式
  3. 計算狀態(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");
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容