美文网首页Android开发之散记AndroidAndroid
Android开发十五《综合技术》

Android开发十五《综合技术》

作者: 独自闯天涯的码农 | 来源:发表于2022-03-28 20:57 被阅读0次

    一、使用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系统Dalvik虚拟机执行的classes.dex文件,该工具位于(Android\sdk\build-tools\25.0.0\dx.bat)。
    任何第三方的libraries和.class文件都会被转换成.dex文件。dx工具的主要工作是将Java字节码转成成Dalvik字节码、压缩常量池、消除冗余信息等。

    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文件生成,它必须被签名才能被安装在设备上。
    在开发过程中,主要用到的就是两种签名的keystore。一种是用于调试的debug.keystore,它主要用于调试,在Eclipse或者Android Studio中直接run以后跑在手机上的就是使用的debug.keystore。
    另一种就是用于发布正式版本的keystore。

    7. 对签名后的APK文件进行对齐处理

    如果你发布的apk是正式版的话,就必须对APK进行对齐处理,用到的工具是zipalign(E:\Documents\Android\sdk\build-tools\25.0.0\zipalign.exe)
    对齐的主要过程是将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快。对齐的作用就是减少运行时内存的使用。

    四、Android Apk安装过程

    Apk安装的主要步骤:

    1. 将apk文件复制到data/app目录
    2. 解析apk信息
    3. Dalvik虚拟机执行dexopt操作(优化.dex文件成odex文件到data/dalvik-cache);如果是ART虚拟机执行dex2oat操作(将.dex文件翻译成.oat文件)
    4. 更新权限信息
    5. 完成安装,发送Intent.ACTION_PACKAGE_ADDED广播
    安装过程

    五、Android的动态加载技术

    动态加载技术也叫插件化技术,通过插件化来减轻应用的内存和CPU占用。
    主要解决三个基础性问题:

    1、资源访问

    ContextImpl中有两个抽象方法:getAssets和getResources方法

    2、Activity生命周期管理

    将Activity生命周期方法提取出来作为一个接口,通过代理Activity去调生命周期方法。

    3、ClassLoader的管理

    同一个插件采用同一个ClassLoader加载类。

    六、Android权限机制

    Android将安全设计贯穿系统架构的各个层面,覆盖系统内核、虚拟机、应用程序框架层以及应用层各个环节,力求在开放的同时,也最大程度地保护用户的数据、应用程序和设备的安全。Android安全模型主要提供以下几种安全机制:


    安全机制

    1、权限的本质

    在 Android 中,一个权限,本质上是一个字符串,一个可以表示执行特定操作的能力的字符串。访问 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 级别的权限是最严格的权限,只会赋予与声明权限使用相同证书的应用程序。

    以系统内置 signature 级别权限为例,Android 系统应用的签名由平台密钥签发,默认情况下源码树里有 4 个不同的密钥文件:platform、shared、media 和 testkey。所有核心平台的包(如:设置、电话、蓝牙)均使用 platform 密钥签发;搜索和通讯录相关的包使用 shared 签发;图库和媒体相关的包使用 media 密钥签发;其他的应用使用 testkey 签发。定义系统内置权限的 framework-res.apk 文件是使用平台密钥签发的,因此任何试图请求 signature 级别内置权限的应用程序,需要使用与框架资源包相同的密钥进行签名。

    • signatureOrSystem 级别:
      可以看做是一种折中的级别,可被赋予与声明权限具有相同签名证书密钥的应用程序(同 signature 级别)或者系统镜像的部分应用,也就是说这允许厂商无须共享签名密钥。Android 4.3 之前,安装在 system 分区下的应用会被自动赋予该保护级别的权限,而 Android 4.4 之后,只允许安装在 system/priv-app/ 目录下的应用才能被主动赋予。

    3、权限的管理

    在每个应用安装时,权限就已经赋予了,系统使用包管理服务来管理权限。打开我们系统目录下的 /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以上动态添加到以上位置
    系统进程的权限配置信息在
    Android\system\core\include\private\android_filesystem_config.h
    

    注意:
    PID:表示应用的进程 ID,PPID 表示父进程 ID;
    UID:UID代表一个应用;应用安装时分配一个唯一的;

    5、权限的检查

    1. 系统内核层权限检查

    内核代码中current_has_network(void) 方法检查了进程的所在组。如果不在 inet 组,则直接返回错误。设置申请权限,经过解析,逐步映射到内核层的组 ID 和用户 ID,最终才能通过内核层的检查。

    2. 框架层权限检查

    Android 6.0 之前组件不能在运行时改变权限,所以系统的权限检查执行过程是静态的

    • 动态权限执行:
      通过IPC:Android 的核心系统服务统一会注册到服务管理器,系统服务可以直接检查调用者的 UID,通过限定 UID 来控制访问权限;不适合非固定UID的应用,适合只允许以 root(UID:0) 或 system(UID:1000) 运行的进程访问的服务检查。
    • 静态权限执行
      跨应用组件交互
      我们使用隐式 Intent 来表达意图,搜索匹配的组件,如果有多个,弹出选择框,目标组件被选定后,会由 ActivityManagerService 执行权限检查,检查目标组件是否有相应的权限要求,如果有,则把权限检查的工作交给 PMS,去检查调用者有没有被授权这些权限。
      接下来的总体的流程和动态执行流程大致相同:Binder.getCallingUid()和Binder.getCallingPid()获取调用者的 UID 和 PID,然后利用 UID 映射包名,再获得相关权限集合。如果权限集合中含有所需权限即启动,否则抛出 SecurityException 异常。静态权限执行这里,我们可以详细了解下,每种组件的权限检查时机和具体顺序是怎么样的。
    • 组件权限执行
    Activity

    会在 startActivity() 和 startActivityForResult() 里解析到声明权限的 Activity 时,就执行权限检查。

    Service

    startService()、stopService() 和 bindService(),这 3 个方法被调用时都会进行权限检查。

    BroadCastReceiver

    发送广播除了常用的 sendBroadcast(Intent intent),还有个 sendBroadcast(Intent intent, String receiverPermission),该方法可以要求广播接受者具备特定的权限,但是,调用 sendBroadcast 是不会进行权限检查的,因为广播是异步的,所以权限检查会在 intent 传递到已注册的广播接受者时进行,如果接收者不具备特定的权限,则不会接收到该广播,也不会收到 SecurityException 异常。

    反过来,接收者可以要求广播发送者必须具备的权限,所要求的权限在 manifest 文件中设置 <receiver> 标签的 permission 属性,或者动态注册时指定 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler),权限检查也是在广播传递时执行。

    所以,收发广播可以分开指定权限。值得一提的是,一些系统广播被声明为 protected,并且只能由系统进程发送,比如 PACKAGE_INSTALLED。只能由系统进程发送,这个限制会在内核层进行检查,对调用者的 UID 进行匹配,只能是 SYSTEM_UID、PHONE_UID、SHELL_UID、BLUETOOTH_UID 或 root。如果其他 UID 的进程试图发送系统广播,则会收到 SecurityException 异常。

    ContentProvider

    ContentProvider 可以为读写分别指定不同的权限,即:调用目标 provider、query() 方法 和 insert()、update()、delete() 都会进行权限检查。

    总结:
    Android 的权限的检查会在各个层次上实施。
    1、高层的组件,例如应用和系统服务,通过包管理器查询应用程序被赋予的权限,并决定是否准予访问。
    2、低层的组件,通常不访问包管理器,比如本地守护进程,依赖于进程的 UID、GID 和补充 GID 来决定赋予。
    3、访问系统资源时,如设备文件、UNIX 域套接字和网络套接字,则由内核根据所有者、目标资源的访问权限和访问进程的进程属性或者 packages.list 来进行控制。

    共享 UID
    最后简单说下共享 UID,填一下前面挖的坑。虽说 Android 会为每一个应用分配唯一的 UID,但如果应用使用相同的密钥签发,就可以使用相同 UID 运行,也就是运行在同一个进程中。
    这个特性被系统应用和核心框架服务广泛使用,比如:Google Play 和 Google 定位服务,请求同一进程内的 Google 登录服务,从而达到静默自动同步用户数据的体验。
    值得注意的是:Android 不支持将一个已安装的应用,从非共享 UID 切换到共享状态,因为改变了已安装应用的 UID,会导致应用失去对自己文件的访问权限(在一些早期 Android 版本中),所以如果使用共享 UID 必须从一开始就设计好。

    参考:你真的了解Android权限机制吗

    七、Android刘海屏适配

    1、AndroidP刘海屏的适配:

    Android P 支持最新的全面屏以及为摄像头和扬声器预留空间的凹口屏幕。通过全新的 DisplayCutout 类,可以确定非功能区域的位置和形状,这些区域不应显示内容。要确定这些凹口屏幕区域是否存在及其位置,使用 getDisplayCutout() 函数。

    1. 设置LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式
    2. 设置沉浸式布局模式
    3. 计算状态栏高度,进行布局;如果有特殊UI要求,则可以使用DisplayCutoutDemo类去获取刘海屏的坐标,完成UIAndroid P 中 WindowManager.LayoutParams 新增了一个布局参数属性layoutInDisplayCutoutMode:
    DisplayCutout 类方法 说明
    getBoundingRects() 返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形
    getSafeInsetLeft () 返回安全区域距离屏幕左边的距离,单位是px
    getSafeInsetRight () 返回安全区域距离屏幕右边的距离,单位是px
    getSafeInsetTop () 返回安全区域距离屏幕顶部的距离,单位是px
    getSafeInsetBottom() 返回安全区域距离屏幕底部的距离,单位是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表示一个不可显示的区域,即刘海屏,可以遍历这个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完全包含在系统栏中时,才允许窗口延伸到DisplayCutout区域。 否则,窗口布局不与DisplayCutout区域重叠。
    LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 该窗口决不允许与DisplayCutout区域重叠。
    LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。Android P 之前的刘海屏适配

    2、AndroidP之前刘海屏的适配

    不同厂商的刘海屏适配方案不尽相同,需分别查阅各自的开发者文档。

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

    相关文章

      网友评论

        本文标题:Android开发十五《综合技术》

        本文链接:https://www.haomeiwen.com/subject/mxnxrrtx.html