Android 基础架构组面试题 | 面试

2022-03-06 09:57:20 浏览数 (1)

theme: smartblue

水一篇

这个应该是今年最后一篇文章了,卷累了打算明年在卷了。

年中的时候帮部门招人,发现很多候选人对于我们部门还是很青睐的。也对鸡架部门做的事比较感兴趣,所以今天这篇水文主要就给大家梳理下基架的面试题以及基础架构组涉及的sdk相关。

因为最近几年面试经常被人吊打,所以也有了总结面试题的习惯。之后加上之前帮候选人的面试总结,今天给大家再卷一波。

也不是别的啥就是想要毛面试题的流量了呢。

SDK相关

面试的时候我觉得哦,这些sdk有任意其实你研究的比较深入就行了,应该能在面试中表现的很好了。还有就是个人建议最好还是在单一方向研究的更深入一点,别的只要大概知道干什么的就行了。

  1. 配置中心以及灰度测试

app必备工具之一,配置中心主要负责的就是动态化的配置,比如文本展示类似这些的。sdk提供方需要负责的是提供动态更新能力,这里有个差异化更新,只更新dif部分,还有就是流量优化等等需要开发同学考虑的。然后可以考虑下存储性能方面的提升等。

而abtest也是app必备工具之一了,动态的下发实验策略,之后开发同学可以切换实验的页面。另外主要需要考虑灰度结果计算,分桶以及版本过滤白名单等等。这里只是一个简单的介绍不展开,因为我只是一个使用方。

  1. 调试组件

个人还是更推荐滴滴的Dokit,功能点比较多而且接入相对来说比较简单。而且提供了很多给开发同学定制的能力,可以在debug情况下增加很多业务相关的测试功能,方便测试同学,核心还是浮窗太方便了。

当然很多实验性的预研功能等其实都可以直接接在这里,然后在测试环境下充分展开,之后在进行线上灰度方案。还有一些具有风险的hook操作,个人也比较建议放在debug组件上。

  1. 性能监控框架

这部分有几个不同的方面,首先是异常崩溃方面的,另外则是性能监控方面的,但是他们整体是划分在一起的,都属于线上性能监控体系的。

Crash相关的,可以从爱奇艺的xCrash学起。包含了崩溃日志,ANR以及native crash,因为版本适配的问题ANR在高版本上已经不是这么好捞了,还有就是native crash相关的。是一个非常牛逼的库了。

而线上的性能监控框架可以从腾讯的Matrix学起,以前有两篇文章介绍的内容也都是和Matrix相关的, Matrix首页上也有介绍,比如fps,卡顿,IO,电池,内存等等方面的监控。其中卡顿监控涉及到的就是方法前后插桩,同时要有函数的mapping表,插桩部分整体来说比较简单感觉。

另外关于线上内存相关的,推荐各位可以学习下快手的koom, 对于hprof的压缩比例听说能达到70%,也能完成线上的数据回捞以及监控等等,是一个非常屌的框架。下面给大家一个抄答案的方式。字节也有一个类似的原理其实也差不多。

主进程发现内存到达阈值的时候,用leakcanary的方案,通过shark fork进程内存,之后生成hrop。由于hrop文件相对较大,所以我们需要对于我们所要分析的内容进行筛选,可以通过xhook,之后对hrop的写入操作进行hook,当发现写入内容的类型符合我们的需要的情况下才进行写入。 而当我们要做线上日志回捞的情况,需要对hprof 进行压缩,具体算法可以参考koom/raphel,有提供对应的压缩算法。 最后线上回捞机制就是基于一个指令,回捞线上符合标准的用户的文件操作,这个自行设计。

其实上述几个库都还是有一个本质相关的东西,那么就是plthook,这个上面三个库应该都有对其的使用,之前是爱奇艺的xhook,现在是字节的bhook, 这个大佬也是我的偶像之一了,非常离谱了算是。

Android 性能采集之Fps,Memory,Cpu 和 Android IO监控

最近已经不咋写这部分相关了,所以也就没有深挖,但是后续可能会有一篇关于phtead hook相关的,也是之前matrix更新的一个新东西,还在测试环境灰度阶段。

  1. 基础网络组件

虽然核心可能还是三方网络库,但是因为基本所有公司都对网络方面有调整和改动,以及解析器等方面的优化,其实可以挖的东西也还是蛮多的。

应付面试的同学可以看看Android网络优化方案。当然还是要具体问题具体分析,毕竟头疼医头,脚疼医脚对吧。

之前和另外一个朋友聊了下,其实很多厂对json解析这部分有优化调整,通过apt之后更换原生成原生的解析方式,加快反序列化速度的都是可以考虑考虑的。

  1. 埋点框架

其实这个应该要放在更前面一点的,数据上报数据分析啥的其实都还是蛮重要的。

这部分因为我完全没写过哦,所以我压根不咋会,但是如果你会的话,面试的时候展开说说,可以帮助你不少。

另外还需要有线上的异常用户数据回捞系统,方便开发同学主动去把线上有异常的用户的日志给收集回来。

但是有些刁钻的页面曝光监控啦,自动化埋点啥的其实还是写过一点的,有兴趣的可以翻翻历史,还有github 上还有demo。

AndroidAutoTrack demo工程

  1. 启动相关

通过DAG(有向无环图)的方式将sdk的初始化拆解成一个个task,之后理顺依赖关系,让他们能按照固定的顺序向下执行。

核心需要处理的是依赖关系,比如说其实埋点库依赖于网络库初始化,然后APM相关的则依赖于埋点库和配置中心abtest等等,这样的依赖关系需要开发同学去理顺的。

另外就是把sdk的粒度打的细碎一点,更容易观察每个sdk任务的耗时情况,之后增加task阈值告警,超过某个加载速度就通知到相应的同学改一下。

多线程是能优化掉一部分,但是也需要避免频繁线程调度。还有就是我个人觉得这些启动相关的东西因为都无法使用sdk级别的灰度,所以改动最好慎重一点。出发点始终都是好的,但是还是结果导向吧。

启动优化的核心,我个人始终坚持的就是延迟才能优化。开发人员很难做到优化代码执行的复杂度,执行时间之类的。尽人事听天命,玄学代码。

  1. 中间件(图片 日志 存储 基础信息)

这部分没啥,最好是对第三方库有一层隔离的思维,但是这个隔离也需要对应的同学对于程序设计方面有很好的思维,说起来简单,其实也蛮复杂的。

这里就不展开了,感觉面试也很少会问的很细。

  1. 第三方sdk大杂烩(偏中台方向)

基本一个app现在都有啥分享啦,推送啦,支付啦,账号体系啦,webview,jsbridge等等服务于应用内的一些sdk,这些东西就比较偏向于业务。

有兴趣的可以看看之前写的两篇关于sdk设计相关的。

活学活用责任链 SDK开发的一点点心得 Android厂商推送Plugin化

  1. 其他方面

大公司可能都会有些动态化方案的考虑,比如插件化啊动态化之类的。这部分在下确实不行,我就不展开了啊。

编译相关

  1. 描述下android编译流程

基架很容易碰到的面试题,以前简单的描述写过。聊聊Android编译流程

虽然是几年前的知识点了,但是还是要拆开高低版本的agp做比较的。所以这部分可以回答下,基本这题就能简单的拿下了。

  1. Gradle 生命周期

简单的说下就是buildSrc先编译,之后是根目录的settings.gradle, 根build.gradle,最后才是module build

网上一堆,你自己翻一番就知道了。

  1. apt是编译中哪个阶段

APT解析的是java 抽象语法树(AST),属于javac的一部分流程。大概流程:.java -> AST -> .class

聊聊AbstractProcessor和Java编译流程

  1. Dex和class有什么区别

链接传送门

Class与dex的区别

1)虚拟机: class用jvm执行,dex用dvm执行

2)文档: class中冗余信息多,dex会去除冗余信息,包含所有类,查找方便,适合手机端

JVM与DVM

1)JVM基于栈(使用栈帧,内存),DVM基于寄存器,速度更快,适合手机端

2)JVM执行Class字节码,DVM执行DEX

3)JVM只能有一个实例,一个应用启动运行在一个DVM

DVM与ART

1)DVM:每次运行应用都需要一次编译,效率降低。JIT

2)ART:Android5.0以上默认为ART,系统会在进程安装后进行一次预编译,将代码转为机器语言存在本地,这样在每次运行时不用再进行编译,提高启动效率;。 AOP & JIT

  1. Transform是如何被执行的

Transform 在编译过程中会被封装成Task 依赖其他编译流程的Task执行。

  1. Transform和其他系统Transform执行的顺序

其实这个题目已经是个过期了,后面对这些都合并整合了,而且最新版的api也做了替换,要不然考虑下回怼下面试官?

Transform和Task之间有关?

  1. 如何监控编译速度变慢问题
代码语言:javascript复制
./gradlew xxxxx -- scan

之后会生成一个gradle的网页,填写下你的邮箱就好了。

另外一个相对来说比较简单了。通过gradle原生提供的listener进行就行了。

代码语言:javascript复制
// 耗时统计kt化
class TimingsListener : TaskExecutionListener, BuildListener {
    private var startTime: Long = 0L
    private var timings = linkedMapOf<String, Long>()


    override fun beforeExecute(task: Task) {
        startTime = System.nanoTime()
    }

    override fun afterExecute(task: Task, state: TaskState) {
        val ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
        task.path
        timings[task.path] = ms
        project.logger.warn("${task.path} took ${ms}ms")
    }

    override fun buildFinished(result: BuildResult) {
        project.logger.warn("Task timings:")
        timings.forEach {
            if (it.value >= 50) {
                project.logger.warn("${it.key} cos  ms  ${it.value}n")
            }
        }
    }

    override fun buildStarted(gradle: Gradle) {

    }

    override fun settingsEvaluated(settings: Settings) {
    }

    override fun projectsLoaded(gradle: Gradle) {

    }

    override fun projectsEvaluated(gradle: Gradle) {

    }

}

gradle.addListener(TimingsListener())
  1. Gradle中如何给一个Task前后插入别的任务

最简单的可以考虑直接获取到Task实例,之后在after和before插入一些你所需要的代码。

另外一个就是通过dependOn前置和finalizedBy挂载一个任务 mustAfter

Gradle 使用指南 -- Gradle Task

  1. ksp APT Transform的区别

ksp 是kotlin专门独立的ast语法树

apt 是java 的ast语法树

transform是 agp 专门修改字节码的一个方法。

反杀时刻AsmClassVisitorFactory,可以看看我之前写的那篇文章。

  1. Transform上的编译优化能做哪些?

虽然是个即将过期的api,但是大家对他的改动还是都比较多的。

首先肯定是需要完成增量编译的,具体的可以参考我的demo工程。记住,所有的transfrom都要全量。

另外可以考虑多线程优化,将转化操作移动到子线程内,建议使用gradle内部的共享线程。

参考agp最新做法,抽象出一个新的interface,之后通过spi串联,之后将asm链式调用。我的文章也介绍过,具体的点在哪里自己盘算。

现在准备好告别Transform了吗

  1. aar 源码切换插件原理

这个前几天刚介绍过,原理和方案业内都差不多,mulite-repo应该都需要这个东西的。我的版本也比较简陋,大厂内部肯定都会有些魔改的。

相对来说功能肯定会更丰富,更全面一点。

aar和源码切换插件Plus

  1. 你们有哪些保证代码质量的手段

最简单的方式还是通过静态扫描 pipline 处理,之后在合并mr之前进行一次拦截。

静态扫描方式比较多,下面给大家简单的介绍下

阿里的sonar 但是对kt的支持很糟糕,因为阿里使用,所以有很多现成的规则可以使用,但是如果从0-1接入,你可能会直接放弃。

原生的lint,可以基于原生提供的lint api,对其进行开发,支持种类也多,基本上算是一个非常优秀的方案了,但是由于文档资料较少,对于开发的要求可能会较高。

AndroidLint

  1. 如何对第三方的依赖做静态检查?

魔高一尺道高一丈。lint还是能解决这个问题的。

Tree Api ClassScanner = 识别三方隐私权限调用

  1. R.java code too large 解决方案

又是一个过期的问题,尽早升级agp版本,让R8帮你解决这个问题,R文件完全可以内联的。

或者用别的AGP插件的R inline也可以解决这个问题。

  1. R inline 你需要注意些什么?

预扫描,先收集调用的信息,之后在进行替换。还有javac 的时候可能就因为文件过大,直接挂掉了。

  1. 一个类替换父类 比如所有activity实现类替换baseactivity

class node 直接替换 superName ,想起了之前另外一个问题,感觉主要是要对构造函数进行修改,否则也会出异常。

  1. R8 D8 以及混淆相关的,还有R8除了混淆还能干些什么? 混淆规则有没有碰到什么奇怪的问题? D8Dx的区别,主要涉及到编译速度以及编译产物的体积,包体积大概小11%。

R8 则是变更了整个编译流程的,其中我觉得最微妙的就是java8 lambda相关的,脱糖前后的差别还是比较大的。同时R8也少了很多之前的Transform。

R8的混淆部分,混淆除了能增加代码阅读难度意外,更多的是对于代码优化方面的。 比如无效代码优化, 同时也删除代码等等都可以做。

  1. 编译的时候有没有碰到javac的常量优化

javac会将静态常量直接优化成具体的数值。但是尤其是多模块场景下尤其容易出现异常,看起来是个实际的常量引用,但是产物上却是一个具体的常量值了。

其他部分

组件化相关

不仅仅要聊到路由,还需要聊下业务仓库的设计,如何避免两个模块之间相互相互引用导致的环问题。

另外就是路由的apt aop的部分都可以深入的聊一下。

如果只聊路由的话,你就只说了一个字符串匹配规则,非常无聊了。

路由跳转

路由跳转只是一小部分,其核心原理就是字符串匹配,之后筛选出符合逻辑的页面进行跳转。

另外就是拦截器的设计,同步异步拦截器两种完全不同的写法。

其原理基于apt transform ,apt负责生成模块德 路由表,而transform则负责将各个模块的路由表进行收集。

服务发现

类似路由表,但是维护的是一个基于键值的类构造。ab之间当有相互依赖的情况下,可以通过基于接口编程的方式进行调整,互相只依赖抽象的接口,之后实现类在内部,通过注册的机制。之后在实际的使用地方用服务发现的机制寻找。

依赖注入

和服务发现类似,也是拿来解决不同模块间的依赖问题。可以使用hilt,依赖注入的好处就是连构造的这部分工作也有di完成了,而且构造能力更多样。可以多参数构造。

虚拟机部分

很多人会觉得虚拟机这部分都是硬八股,比较无聊。但是其实有时候我们碰到的一些字节码相关的问题就和这部分基础姿势相关了。

虽然用的比较少,但是也不是一个硬八股,比hashmap好玩太多了。

总结

其实以当前来说安卓的整个体系相对来说很复杂,第三方库以及源代码量都比较大,并不是要求每个同学都对这些有一个良好的掌握,但是大体上应该了解的还是需要了解的。

面试造火箭可不是浪得虚名啊,但是鸡架可能还是需要使用到其中一些奇奇怪怪的黑科技的。

好了胡扯结束了,今天的文章就到此为止了。

0 人点赞