Android之进阶总结篇

Android面试之进阶杂谈

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

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

  • 本地测试:只在本地机器 JVM上 运行,以最小化执行时间,这种单元测试不依赖于 Android 框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离 Android 依赖的目的,模拟框架如 Google 推荐的 Mockito;
  • 检测测试:真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式;
  • 注意:单元测试不适合测试复杂的UI交互事件
  • App的稳定主要决定于整体的系统架构设计,同时也不可忽略代码编程的细节规范,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃,所以上线之前除了自己 本地化测试 之外还需要进行 Monkey压力测试
  • 少部分面试官可能会延伸,如Gradle自动化测试、机型适配测试等

2. Android中如何查看一个对象的回收情况 ?

  1. 首先要了解 Java 四种引用类型的场景和使用(强引用、软引用、弱引用、虛引用);
  2. 举个场景例子:SoftReference 对象是用来保存软引用的,但它同时也是一个Java对象,所以当软引用对象被回收之后,虽然这个 SoftReference 对象的 get 方法返回 null,但 SoftReference 对象本身并不是 null,而此时这个 SoftReference 对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的 内存泄露
  3. 因此,Java 提供 ReferenceQueue 来处理引用对象的回收情况。当 SoftReference 所引用的对象被 GC 后,JVM会先将 softReference 对象添加到 ReferenceQueue 这个队列中。当我们调用 ReferenceQueue 的 poll() 方法,如果这个队列中不是空队列,那么将返回并移除前面添加的那个 Reference 对象。
    在这里插入图片描述

3. Apk的大小如何压缩 ?

3.1 一个完整APK包含以下目录(将APK文件拖到Android Studio):

  1. META-INF/:包含 CERT.SFCERT.RSA 签名文件以及 MANIFEST.MF 清单文件。
  2. assets/:包含应用可以使用 AssetManager 对象检索的应用资源。
  3. res/:包含未编译到的资源 resources.arsc。
  4. lib/:包含特定于处理器软件层的编译代码。该目录包含了每种平台的子目录,像armeabi,armeabi-v7a, arm64-v8a,x86,x86_64,和mips。
  5. resources.arsc:包含已编译的资源。该文件包含res/values/ 文件夹所有配置中的XML内容。打包工具提取此XML内容,将其编译为二进制格式,并将内容归档。此内容包括语言字符串和样式,以及直接包含在**resources.arsc*8文件中的内容路径 ,例如布局文件和图像。
  6. classes.dex:包含以Dalvik / ART虚拟机可理解的DEX文件格式编译的类。
  7. AndroidManifest.xml:包含核心Android清单文件。该文件列出应用程序的名称,版本,访问权限和引用的库文件。该文件使用Android的二进制XML格式。
    在这里插入图片描述
  8. lib、class.dex 和 res 占用了超过 90% 的空间,所以这三块是优化 Apk 大小的重点(实际情况不唯一)

3.2 减少res,压缩图文文件

  1. 图片文件压缩是针对 jpg 和 png 格式的图片。我们通常会放置多套不同分辨率的图片以适配不同的屏幕,这里可以进行适当的删减。在实际使用中,只保留一到两套就足够了(保留一套的话建议保留 xxhdpi,两套的话就加上 hdpi),然后再对剩余的图片进行压缩( jpg 采用优图压缩,png 尝试采用 pngquant 压缩)

3.3 减少dex文件大小

  1. 添加资源混淆
    在这里插入图片描述
  2. shrinkResources 为 true 表示移除未引用资源,和代码压缩协同工作。
  3. minifyEnabled 为 true 表示通过 ProGuard 启用代码压缩,配合 proguardFiles 的配置对代码进行混淆并移除未使用的代码。
  4. 代码混淆在压缩 apk 的同时,也提升了安全性。

3.4 减少lib文件大小

  1. 由于引用了很多第三方库,lib 文件夹占用的空间通常都很大,特别是有 so 库的情况下。很多so库会同时引入 armeabi、armeabi-v7a 和 x86 这几种类型,这里可以只保留 armeabi 或 armeabi-v7a 的其中一个就可以了,实际上微信等主流 app 都是这么做的;
  2. 只需在 build.gradle 直接配置即可,NDK 配置同理;
    在这里插入图片描述

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

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

    2. 接着在 app 目录下的 build.gradle 中配置 productFlavors,也就是配置打包的渠道:
      在这里插入图片描述

    3. 最后在编辑器下方的Teminal输出命令行

      1. 执行./gradlew assembleRelease ,将会打出所有渠道的release包;
      2. 执行./gradlew assembleVIVO,将会打出VIVO渠道的release和debug版的包;
      3. 执行./gradlew assembleVIVORelease将生成VIVO的release包。

5. 插件化原理分析

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

    1. 宿主: 就是当前运行的APP。
    2. 插件: 相对于插件化技术来说,就是要加载运行的apk类文件。
  2. 热修复 则是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。
    在这里插入图片描述

  3. 类加载机制

    1. Android 中常用的两种类加载器,DexClassLoader 和 PathClassLoader,它们都继承于 BaseDexClassLoader,两者区别在于 PathClassLoader 只能加载内部存储目录的 dex/jar/apk 文件。 DexClassLoader 支持加载指定目录(不限于内部)的 dex/jar/apk 文件;
  4. 插件通信:通过给插件apk生成相应的 DexClassLoader 便可以访问其中的类,可分为单 DexClassLoader 和多 DexClassLoader 两种结构。

    1. 若使用 多 ClassLoader 机制,主工程引用插件中类需要先通过插件的 ClassLoader 加载该类再通过 反射调 用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。
    2. 若使用 单 ClassLoader 机制,主工程则可以 直接通过 类名去访问插件中的类。该方式有个弊端,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错。
  5. 资源加载

    1. 原理在于通过反射将插件apk的路径加入 AssetManager 中并创建 Resource 对象加载资源,有两种处理方式:
      1. 合并式:addAssetPath 时加入所有插件和主工程的路径;由于AssetManager 中加入了所有插件和主工程的路径,因此生成的 Resource 可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源 id 会存在相同的情况,在访问时会产生资源冲突。
      2. 独立式:各个插件只添加自己 apk 路径,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的 Resource 对象。

6. 组件化原理

  1. 引入组件化的原因:项目随着需求的增加规模变得越来越大,规模的增大导致了各种业务错中复杂的交织在一起, 每个业务模块之间,代码没有约束,带来了代码边界的模糊,代码冲突时有发生, 更改一个小问题可能引起一些新的问题, 牵一发而动全身,增加一个新需求,需要熟悉相关的代码逻辑,增加开发时间;
    1. 避免重复造轮子,可以节省开发和维护的成本。
    2. 可以通过组件和模块为业务基准合理地安排人力,提高开发效率。
    3. 不同的项目可以共用一个组件或模块,确保整体技术方案的统一性。
    4. 为未来插件化共用同一套底层模型做准备。
  2. 组件化开发流程就是把一个功能完整的App或模块拆分成 多个子模块(Module),每个子模块可以 独立编译运行,也可以任意组合成另一个新的 App或模块,每个模块即不相互依赖但又可以相互交互,但是最终发布的时候是将这些组件合并统一成一个apk,遇到某些特殊情况甚至可以 升级 或者 降级
  3. 举个简单的模型例子
    在这里插入图片描述
    在这里插入图片描述
    App 是主 application,ModuleA 和 ModuleB 是两个业务模块(相对独立,互不影响),Library 是基础模块,包含所有模块需要的依赖库,以及一些工具类:如网络访问、时间工具等;
  4. 注意:提供给各业务模块的基础组件,需要根据具体情况拆分成 aar 或者 library,像登录,基础网络层这样较为稳定的组件,一般直接打包成 aar,减少编译耗时。而像自定义 View 组件,由于随着版本迭代会有较多变化,就直接以源码形式抽离成 Library;

7. 跨组件通信

7.1 跨组件通信场景:

  1. 第一种是组件之间的页面跳转 (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型);
  2. 第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)。

7.2 跨组件通信方案分析:

  1. 第一种组件之间的页面跳转实现简单,跳转时想传递不同类型的数据提供有相应的 API 即可。
  2. 第二种组件之间的自定义类和自定义方法的调用要稍微复杂点,需要 ARouter 配合架构中的 公共服务 (CommonService) 实现:
    1. 提供服务的业务模块:
    2. 在公共服务 (CommonService) 中声明 Service 接口 (含有需要被调用的自定义方法), 然后在自己的模块中实现这个 Service 接口, 再通过 ARouter API 暴露实现类。
    3. 使用服务的业务模块:
      1. 通过 ARouter 的 API 拿到这个 Service 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互。
    4. 此外,可以使用 AndroidEventBus 其独有的 Tag, 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 Tag 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 EventBus。

7.3 如何管理过多的路由表?

  1. RouterHub 存在于基础库, 可以被看作是所有组件都需要遵守的通讯协议, 里面不仅可以放路由地址常量, 还可以放跨组件传递数据时命名的各种 Key 值, 再配以适当注释, 任何组件开发人员不需要事先沟通只要依赖了这个协议, 就知道了各自该怎样协同工作, 既提高了效率又降低了出错风险, 约定的东西自然要比口头上说强。
  2. Tips: 如果您觉得把每个路由地址都写在基础库的 RouterHub 中, 太麻烦了, 也可以在每个组件内部建立一个私有 RouterHub, 将不需要跨组件的路由地址放入私有 RouterHub 中管理, 只将需要跨组件的路由地址放入基础库的公有 RouterHub 中管理, 如果您不需要集中管理所有路由地址的话, 这也是比较推荐的一种方式。

7.4 ARouter路由原理:

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

7.5 常见的组件化方案如下

在这里插入图片描述


8. 组件化中路由、埋点的实现

8.1 组件通信方案—— 路由(Router)

  1. 因为在组件化中,各个业务模块之间是各自 独立 的, 并不会存在相互依赖的关系, 所以一个业务模块是访问不了其他业务模块的代码的, 如果想从 A 业务模块的 A 页面跳转到 B 业务模块的 B 页面, 光靠模块自身是不能实现的,这就需要一种跨组件通信方案—— 路由(Router)

8.2 路由主要有以下两种场景:

  1. 第一种是组件之间的 页面跳转 (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)
  2. 第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)

8.3 路由原理

  1. 其原理在于将分布在不同组件module中的某些类按照一定规则生成映射表(数据结构通常是Map,Key为一个字符串,Value为类或对象),然后在需要用到的时候从映射表中根据字符串从映射表中取出类或对象,本质上是类的查找

8.4 埋点

  1. 埋点是在应用中特定的流程收集一些信息,用来跟踪应用使用的状况;
  2. 代码埋点:在某个事件发生时调用SDK里面相应的接口发送埋点数据,百度统计、友盟、TalkingData、Sensors Analytics等第三方数据统计服务商大都采用这种方案;
  3. 全埋点:全埋点指的是将Web页面/App内产生的所有的、满足某个条件的行为,全部上报到后台服务器;
  4. 可视化埋点:通过可视化工具(例如Mixpanel)配置采集节点,在Android端自动解析配置并上报埋点数据,从而实现所谓的自动埋点;
  5. 无埋点:它并不是真正的不需要埋点,而是Android端自动采集全部事件并上报埋点数据,在后端数据计算时过滤出有用数据。

9. Hook以及插桩技术

9.1 Hook 是?

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

9.2 Android 中的 Hook 机制,大致有两个方式:

  1. 要 root 权限,直接 Hook 系统,可以干掉所有的 App。
  2. 无 root 权限,但是只能 Hook 自身app,对系统其它 App 无能为力。

9.3 插桩是?

  1. 插桩 是以静态的方式修改第三方的代码,也就是从编译阶段,对源代码(中间代码)进行编译,而后重新打包,是 静态的篡改
  2. 而 Hook 则不需要再编译阶段修改第三方的源码或中间代码,是在运行时通过反射的方式修改调用,是一种动态的篡改

10. Android的签名机制?

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

11. v3签名key和v2还有v1有什么区别?

11.1 在v1版本的签名中,

  1. 签名以文件的形式存在于apk包中,这个版本的apk包就是一个标准的zip包,V2和V1的差别是V2是对整个zip包进行签名,而且在zip包中增加了一个apk signature block,里面保存签名信息。
    在这里插入图片描述

11.2 在v2版本签名块(APK Signing Block)本身又主要分成三部分:

  1. SignerData(签名者数据):主要包括签名者的证书,整个APK完整性校验hash,以及一些必要信息
  2. Signature(签名):开发者对SignerData部分数据的签名数据
  3. PublicKey(公钥):用于验签的公钥数据

11.3 v3版本签名块也分成同样的三部分,

  1. 与v2不同的是在SignerData部分,v3新增了attr块,其中是由更小的level块组成。每个level块中可以存储一个证书信息。前一个level块证书验证下一个level证书,以此类推。最后一个level块的证书,要符合SignerData中本身的证书,即用来签名整个APK的公钥所属于的证书。
    在这里插入图片描述

12. Android5.0~10.0之间大的变化

12.1 Android5.0新特性

  1. MaterialDesign设计风格
  2. 支持64位ART虚拟机(5.0推出的ART虚拟机,在5.0之前都是Dalvik。他们的区别是:Dalvik,每次运行,字节码都需要通过即时编译器转换成机器码(JIT)。 ART,第一次安装应用的时候,字节码就会预先编译成机器码(AOT))
  3. 通知详情可以用户自己设计

12.2 Android6.0新特性

  1. 动态权限管理
  2. 支持快速充电的切换
  3. 支持文件夹拖拽应用
  4. 相机新增专业模式

12.3 Android7.0新特性

  1. 多窗口支持
  2. V2签名
  3. 增强的Java8语言模式
  4. 夜间模式

12.4 Android8.0(O)新特性

  1. 优化通知:通知渠道 (Notification Channel) 通知标志 休眠 通知超时 通知设置 通知清除
  2. 画中画模式:清单中Activity设置android:supportsPictureInPicture
  3. 后台限制
  4. 自动填充框架
  5. 系统优化 等等优化很多

12.5 Android9.0(P)新特性

  1. 室内WIFI定位
  2. “刘海”屏幕支持
  3. 安全增强
  4. 等等优化很多

12.6 Android10.0(Q)

  1. 夜间模式:包括手机上的所有应用都可以为其设置暗黑模式。
  2. 桌面模式:提供类似于PC的体验,但是远远不能代替PC。
  3. 屏幕录制:通过长按“电源”菜单中的"屏幕快照"来开启。

13. 说下Measurepec这个类

13.1 作用

  1. 通过宽测量值 widthMeasureSpec 和高测量值 heightMeasureSpec 决定 View 的大小;

13.2 组成

  1. 一个 32 位 int 值,高 2 位代表 SpecMode (测量模式),低 30位 代表 SpecSize( 某种测量模式下的规格大小)。

13.3 三种模式

  1. UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。
  2. EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent或具体数值。
  3. AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。

13.4 决定因素

  1. 值由 子 View 的布局参数 LayoutParams 和父容器的 MeasureSpec 值共同决定。具体规则见下图:
    在这里插入图片描述

14. 请例举Android中常用布局类型,并简述其用法以及排版效率

Android中常用布局分为传统布局和新型布局

14.1 传统布局(编写XML代码、代码生成):

  1. 框架布局(FrameLayout):
  2. 线性布局(LinearLayout):
  3. 绝对布局(AbsoluteLayout):
  4. 相对布局(RelativeLayout):
  5. 表格布局(TableLayout):

14.2 新型布局(可视化拖拽控件、编写XML代码、代码生成):

  1. 约束布局(ConstrainLayout):
    在这里插入图片描述

14.3 对于嵌套多层View而言,其排版效率:

LinearLayout = FrameLayout >> RelativeLayout

15. 区别Animation和Animator的用法,概述其原理

15.1 动画的种类

  1. 前者只有 透明度,旋转,平移,伸缩 4种属性;
  2. 对于后者,只要是该控件的属性,且有 setter 该属性的方法就都可以对该属性执行一种 动态变化 的效果。

15.2 可操作的对象

  1. 前者只能对 UI组件 执行动画,但属性动画几乎可以对 任何对象 执行动画(不管它是否显示在屏幕上)。

15.3 动画播放顺序

  1. 在 Animator 中,AnimatorSet 正是通过 playTogether()、 playSequentially()、animSet.play().with()、before()、after() 这些方法来控制多个动画协同工作,从而做到对动画播放顺序的精确控制
    在这里插入图片描述

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

  1. 图片加载库:Fresco、Glide、Picasso等

16.1 Glide的设计微妙在于:

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

17. 如何绕过9.0限制?

在这里插入图片描述


18. 用过哪些网络加载库?OkHttp、Retrofit实现原理?

  1. 网络加载库:OkHttp、Retrofit、xUtils、Volley等

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

19.1 内部更新:

  1. 通过接口获取线上版本号,versionCode
  2. 比较线上的versionCode 和本地的versionCode,弹出更新窗口
  3. 下载APK文件(文件下载)
  4. 安装APK

19.2 灰度更新:

  1. 找单一渠道投放特别版本。
  2. 做升级平台的改造,允许针对部分用户推送升级通知甚至版本强制升级。
  3. 开放单独的下载入口。
  4. 是两个版本的代码都打到app包里,然后在app端植入测试框架,用来控制显示哪个版本。测试框架负责与服务器端api通信,由服务器端控制app上A/B版本的分布,可以实现指定的一组用户看到A版本,其它用户看到B版本。服务端会有相应的报表来显示A/B版本的数量和效果对比。最后可以由服务端的后台来控制,全部用户在线切换到A或者B版本~
  5. 无论哪种方法都需要做好版本管理工作,分配特别的版本号以示区别。 当然,既然是做灰度,数据监控(常规数据、新特性数据、主要业务数据)还是要做到位,该打的数据桩要打。 还有,灰度版最好有收回的能力,一般就是强制升级下一个正式版。

19.3 强制更新:

  1. 一般的处理就是进入应用就弹窗通知用户有版本更新,弹窗可以没有取消按钮并不能取消。这样用户就只能选择更新或者关闭应用了,当然也可以添加取消按钮,但是如果用户选择取消则直接退出应用。

19.4 增量更新:

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

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

  1. Kotlin 是一种具有类型推断的跨平台,静态类型的通用编程语言。 Kotlin 旨在与 Java 完全互操作,其标准库的 JVM 版本依赖于 Java 类库,但类型推断允许其语法更简洁。
  2. Flutter 是由 Google 创建的开源移动应用程序开发框架。它用于开发 Android 和 iOS 的应用程序,以及为 Google Fuchsia 创建应用程序的主要方法。

声明:本文整理自网络资料,如有侵权请联系。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liusaisaiV1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值