【Android】最新面试题-进阶篇

如何进行单元测试,如何保证 App 稳定 ?

要测试 Android 应用程序,通常会创建以下类型自动单元测试

  1. 本地测试:只在本地机器 JVM 上运行,以最小化执行时间,这种单元测试不依赖于 Android 框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离 Android 依赖的目的,模拟框架如Google 推荐的 Mockito;
  2. 检测测试:真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android 系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式;

注意:单元测试不适合测试复杂的 UI 交互事件

App 的稳定主要决定于整体的系统架构设计,同时也不可忽略代码编程的细节规范,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃,所以上线之前除了自己本地化测试之外还需要进行 Monkey 压力测试

少部分面试官可能会延伸,如 Gradle 自动化测试、机型适配测试等。

Apk 的大小如何压缩

  1. 减少 res,压缩图文文件
  • 图片文件压缩是针对 jpg 和 png 格式的图片。我们通常会放置多套不同分辨率的图片以适配不同的屏幕,这里可以进行适当的删减。在实际使用中,只保留一到两套就足够了(保留一套的话建议保留xxhdpi,两套的话就加上 hdpi),然后再对剩余的图片进行压缩(jpg 采用优图压缩,png 尝试采用pngquant 压缩)
  1. 减少 dex 文件大小
  • 添加资源混淆
  • shrinkResources 为 true 表示移除未引用资源,和代码压缩协同工作。
  • minifyEnabled 为 true 表示通过 ProGuard 启用代码压缩,配合 proguardFiles 的配置对代码进行混淆并移除未使用的代码。
  • 代码混淆在压缩 apk 的同时,也提升了安全性。
  1. 减少 lib 文件大小
  • 由于引用了很多第三方库,lib 文件夹占用的空间通常都很大,特别是有 so 库的情况下。很多 so 库会同时引入 armeabi、armeabi-v7a 和 x86 这几种类型,这里可以只保留 armeabi 或 armeabi-v7a 的其中一个就可以了,实际上微信等主流 app 都是这么做的。
  • 只需在 build.gradle 直接配置即可,NDK 配置同理

如何通过 Gradle 配置多渠道包?

  1. 首先要了解设置多渠道的原因。在安装包中添加不同的标识,配合自动化埋点,应用在请求网络的时候携带渠道信息,方便后台做运营统计,比如说统计我们的应用在不同应用市场的下载量等信息
  2. 这里以友盟统计为例
  • 首先在 manifest.xml 文件中设置动态渠道变量:


  • 接着在 app 目录下的 build.gradle 中配置 productFlavors,也就是配置打包的渠道:


  • 最后在编辑器下方的 Teminal 输出命令行
    执行./gradlew assembleRelease ,将会打出所有渠道的 release 包;
    执行./gradlew assembleVIVO,将会打出 VIVO 渠道的 release 和 debug 版的包;
    执行./gradlew assembleVIVORelease 将生成 VIVO 的 release 包。

插件化原理分析

  1. 插件化是指将 APK 分为宿主和插件的部分。把需要实现的模块或功能当做一个独立的提取出来,在 APP 运行时,我们可以动态的载入或者替换插件部分,减少宿主的规模

宿主: 就是当前运行的 APP。
插件: 相对于插件化技术来说,就是要加载运行的apk 类文件。

  1. 而热修复则是从修复 bug 的角度出发,强调的是在不需要二次安装应用的前提下修复已知的 bug。


  2. 类加载机制
    Android 中常用的两种类加载器,DexClassLoader 和 PathClassLoader,它们都继承于 BaseDexClassLoader,两者区别在于PathClassLoader 只能加载内部存储目录的 dex/jar/apk 文件。DexClassLoader 支持加载指定目录(不限于内部)的 dex/jar/apk 文件

  3. 插件通信:通过给插件 apk 生成相应的 DexClassLoader 便可以访问其中的类,可分为单 DexClassLoader 和多 DexClassLoader 两种结构。

  • 若使用多 ClassLoader 机制,主工程引用插件中类需要先通过插件的 ClassLoader 加载该类再通过反 射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。
  • 若使用单 ClassLoader 机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊端,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错。
  1. 资源加载
    原理在于通过反射将插件 apk 的路径加入 AssetManager 中并创建 Resource 对象加载资源,有两种处理方式:
  • 合并式:addAssetPath 时加入所有插件和主工程的路径;由于 AssetManager 中加入了所有插件和主工程的路径,因此生成的 Resource 可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源 id 会存在相同的情况,在访问时会产生资源冲突。
  • 独立式:各个插件只添加自己 apk 路径,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的 Resource 对象。

组件化原理

  1. 引入组件化的原因:项目随着需求的增加规模变得越来越大,规模的增大导致了各种业务错中复杂的交织在一起,每个业务模块之间,代码没有约束,带来了代码边界的模糊,代码冲突时有发生, 更改一个小问题可能引起一些新的问题, 牵一发而动全身,增加一个新需求,需要熟悉相关的代码逻辑,增加开发时间
  • 避免重复造轮子,可以节省开发和维护的成本。
  • 可以通过组件和模块为业务基准合理地安排人力,提高开发效率。
  • 不同的项目可以共用一个组件或模块,确保整体技术方案的统一性。
  • 为未来插件化共用同一套底层模型做准备。
  1. 组件化开发流程就是把一个功能完整的 App 或模块拆分成多个子模块(Module),每个子模块可以独立编译运行,也可以任意组合成另一个新的 App 或模块,每个模块即不相互依赖但又可以相互交互,但是最终发布的时候是将这些组件合并统一成一个 apk,遇到某些特殊情况甚至可以升级或者降级

  2. 举个简单的模型例子


App 是主 application,ModuleA 和 ModuleB 是两个业务模块(相对独立,互不影响),Library 是基础模块,包含所有模块需要的依赖库,以及一些工具类:如网络访问、时间工具等

  • 注意:提供给各业务模块的基础组件,需要根据具体情况拆分成 aar 或者 library,像登录,基础网络层这样较为稳定的组件,一般直接打包成 aar,减少编译耗时。而像自定义 View 组件,由于随着版本迭代会有较多变化,就直接以源码形式抽离成 Library

跨组件通信

  1. 跨组件通信场景:
  • 第一种是组件之间的页面跳转 (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)。
  • 第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)。
  1. 跨组件通信方案分析:
  • 第一种组件之间的页面跳转实现简单,跳转时想传递不同类型的数据提供有相应的 API 即可。
  • 第二种组件之间的自定义类和自定义方法的调用要稍微复杂点,需要 ARouter 配合架构中的 公共服务(CommonService) 实现:
    1)提供服务的业务模块:
    2)在公共服务(CommonService) 中声明 Service 接口 (含有需要被调用的自定义方法), 然后在自己的模块中实现这个 Service 接口, 再通过 ARouter API 暴露实现类。
    3)使用服务的业务模块:通过 ARouter 的 API 拿到这个 Service 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互。
    4)此外,可以使用 AndroidEventBus 其独有的 Tag, 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 Tag 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 EventBus。
  1. 如何管理过多的路由表?
  • RouterHub 存在于基础库, 可以被看作是所有组件都需要遵守的通讯协议, 里面不仅可以放路由地址常量, 还可以放跨组件传递数据时命名的各种 Key 值,再配以适当注释, 任何组件开发人员不需要事先沟通只要依赖了这个协议, 就知道了各自该怎样协同工作, 既提高了效率又降低了出错风险, 约定的东西自然要比口头上说强。

  • Tips: 如果您觉得把每个路由地址都写在基础库的 RouterHub 中, 太麻烦了, 也可以在每个组件内部建立一个私有 RouterHub, 将不需要跨组件的路由地址放入私有 RouterHub 中管理, 只将需要跨组件的路由地址放入基础库的公有 RouterHub 中管理, 如果您不需要集中管理所有路由地址的话, 这也是比较推荐的一种方式。

  1. ARouter 路由原理:
    ARouter 维护了一个路由表 Warehouse,其中保存着全部的模块跳转关系,ARouter 路由跳转实际上还是调用了 startActivity 的跳转,使用了原生的 Framework 机制,只是通过 apt 注解的形式制造出跳转规则,并人为地拦截跳转和设置跳转条件。

Hook 以及插桩技术

  1. Hook 是一种用于改变 API 执行结果的技术,能够将系统的 API 函数执行重定向(应用的触发事件和后台逻辑处理是根据事件流程一步步地向下执行。而 Hook 的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件,例如逆向破解 App)


  2. Android 中的 Hook 机制,大致有两个方式:
  • 要 root 权限,直接 Hook 系统,可以干掉所有的 App。
  • 无 root 权限,但是只能 Hook 自身 app,对系统其它 App 无能为力。
  1. 插桩是以静态的方式修改第三方的代码,也就是从编译阶段,对源代码(中间代码)进行编译,而后重新打包,是静态的篡改; 而 Hook 则不需要再编译阶段修改第三方的源码或中间代码,是在运行时通过反射的方式修改调用,是一种动态的篡改。

Android 的签名机制

Android 的签名机制包含有消息摘要、数字签名和数字证书:

  • 消息摘要:在消息数据上,执行一个单向的 Hash 函数,生成一个固定长度的Hash 值
  • 数字签名:一种以电子形式存储消息签名的方法,一个完整的数字签名方案应该由两部分组成:签名算法和验证算法
  • 数字证书:一个经证书授权(Certificate Authentication)中心数字签名的包含公钥拥有者信息以及公钥的文件

Android5.0~10.0 之间大的变化

  1. Android5.0 新特性
  • MaterialDesign 设计风格
  • 支持 64 位 ART 虚拟机(5.0 推出的 ART 虚拟机,在 5.0 之前都是 Dalvik。他们的区别是:Dalvik,每次运行,字节码都需要通过即时编译器转换成机器码(JIT)。 ART,第一次安装应用的时候,字节码就会预先编译成机器码(AOT))
  • 通知详情可以用户自己设计
  1. Android6.0 新特性
  • 动态权限管理
  • 支持快速充电的切换
  • 支持文件夹拖拽应用
  • 相机新增专业模式
  1. Android7.0 新特性
  • 多窗口支持
  • V2 签名
  • 增强的 Java8 语言模式
  • 夜间模式
  1. Android8.0 新特性
  • 优化通知:通知渠道 (Notification Channel) 通知标志 休眠 通知超时 通知设置 通知清除
  • 画中画模式:清单中 Activity 设置 android:supportsPictureInPicture
  • 后台限制
  • 自动填充框架
  • 系统优化
  • 等等优化很多
  1. Android9.0(P)新特性
  • 室内 WIFI 定位
  • “刘海”屏幕支持
  • 安全增强
  • 等等优化很多
  1. Android10.0(Q)目前曝光的新特性
  • 夜间模式:包括手机上的所有应用都可以为其设置暗黑模式。
  • 桌面模式:提供类似于 PC 的体验,但是远远不能代替 PC。
  • 屏幕录制:通过长按“电源”菜单中的"屏幕快照"来开启。

使用过什么图片加载库?Glide 的源码设计哪里很微妙?

  1. 图片加载库:Fresco、Glide、Picasso 等
  2. Glide 的设计微妙在于:
  • Glide 的生命周期绑定:可以控制图片的加载状态与当前页面的生命周期同步,使整个加载过程随着页面的状态而启动/恢复,停止,销毁
  • Glide 的缓存设计:通过(三级缓存,Lru 算法,Bitmap 复用)对 Resource 进行缓存设计
  • Glide 的完整加载过程:采用 Engine 引擎类暴露了一系列方法供 Request 操作

对于应用更新这块是如何做的? (灰度,强制更新、分区域更新)

  1. 内部更新:
  • 通过接口获取线上版本号,versionCode
  • 比较线上的 versionCode 和本地的 versionCode,弹出更新窗口
  • 下载 APK 文件(文件下载)
  • 安装 APK
  1. 灰度更新:
  • 找单一渠道投放特别版本。
  • 做升级平台的改造,允许针对部分用户推送升级通知甚至版本强制升级。
  • 开放单独的下载入口。
  • 是两个版本的代码都打到 app 包里,然后在 app 端植入测试框架,用来控制显示哪个版本。测试框架负责与服务器端 api 通信,由服务器端控制 app 上A/B 版本的分布,可以实现指定的一组用户看到 A版本,其它用户看到 B 版本。服务端会有相应的报表来显示 A/B 版本的数量和效果对比。最后可以由服务端的后台来控制,全部用户在线切换到 A 或者 B 版本~
  • 无论哪种方法都需要做好版本管理工作,分配特别的版本号以示区别。 当然,既然是做灰度,数据监控(常规数据、新特性数据、主要业务数据)还是要做到位,该打的数据桩要打。 还有,灰度版最好有收回的能力,一般就是强制升级下一个正式版。
  1. 强制更新:
    一般的处理就是进入应用就弹窗通知用户有版本更新,弹窗可以没有取消按钮并不能取消。这样用户就只能选择更新或者关闭应用了,当然也可以添加取消按钮,但是如果用户选择取消则直接退出应用。

  2. 增量更新:
    二进制差分工具 bsdiff 是相应的补丁合成工具,根据两个不同版本的二进制文件,生成补丁文件.patch 文件。通过 bspatch 使旧的 apk 文件与不定文件合成新的 apk。 注意通过 apk 文件的 md5 值进行区分版本。

会用 Kotlin、Fultter 吗? 谈谈你的理解

  • Kotlin 是一种具有类型推断的跨平台,静态类型的通用编程语言。 Kotlin 旨在与 Java 完全互操作,其标准库的JVM 版本依赖于 Java 类库,但类型推断允许其语法更简洁。
  • Flutter 是由 Google 创建的开源移动应用程序开发框架。它用于开发 Android 和 iOS 的应用程序,以及为 Google Fuchsia 创建应用程序的主要方法
  • 关于 kotlin 的重要性,相信大家在日常开发可以体会到,应用到实际开发中,需要避免语法糖(例如单列模式、空值判断、高阶函数等)
  • 至于 Flutter,目前 Google 官方文档还不完善,市面上采用此语言编写的项目较少,如需要具体深入,请参考闲鱼和官方文档

说一下常见内存泄漏及优化方案

内存泄露在 Android 内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们 不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避 免大多数情况的内存泄漏:

  1. 构造单例的时候尽量别用 Activity 的引用;
  2. 静态引用时注意应用对象的置空或者少用静态引用;
  3. 使用静态内部类+软引用代替非静态内部类;
  4. 及时取消广播或者观察者注册;
  5. 耗时任务、属性动画在 Activity 销毁时记得 cancel;
  6. 文件流、Cursor 等资源及时关闭;
  7. Activity 销毁时 WebView 的移除和销毁。

你可能感兴趣的:(【Android】最新面试题-进阶篇)