美文网首页Android开发
草稿-大厂资深面试官 带你破解Android高级面试(part2

草稿-大厂资深面试官 带你破解Android高级面试(part2

作者: New_X | 来源:发表于2020-01-16 10:03 被阅读0次

第7章 不以为然:我脸上写着我不会 Handler 吗?【Handler相关】

1. Android中为什么非UI线程不能更新UI?

考察点:

  • 是否理解线程安全的概念(中级)
  • 是否能够理解UI线程的工作机制(高级)
  • 是否熟悉SurfaceView实现高帧率的原理(高级)

题目剖析:
关键词:非UI线程、更新UI

  • UI线程的工作机制
  • 为什么UI设计成线程不安全的
  • 非UI线程一定不能更新UI吗

UI线程是什么?
所有app都是通过zygote fork出来的,启动了ActivityThread(main函数),再启动了Looper.loop() --> 就是ActivityThread main函数的线程

通常概念的主线程对应UI线程,其他线程对应非UI线程
(step1:知道UI线程怎么来的)

主线程如何工作 TODO:07:50
(step2:多答出来细节,MessageQueue在Looper有消息,没消息的时候怎么样)

UI为什么不设计成线程安全的 ---> 单线程模型

  • UI具有可变性,甚至是高频可变性
    --> UI对响应时间的敏感性要求UI操作必须高效
  • UI组件必须批量绘制来保证效率
    (加锁是有性能损耗的,UI绘制是高优先级的,宁愿把其他操作拦在外边也不要影响UI的绘制比较好)

非UI线程一定不能更新UI吗?

  • 间接更新:Handler.sendMessage和View.postInvalide都是告诉UI线程需要更新了(当前内容已经失效了,下一帧要记得绘制哦~)
  • 直接更新:SurfaceView(TODO:13:56)在独立的工作线程绘制,UI线程压力就会小很多 --> TextureView(现在比较流行,比如相机预览)
    ---> GLSurfaceView,有GLThread进行绘制,所以UI线程卡了,有些用GLSurfaceView还能继续绘制
    (TextureView和普通View一样,没有独立出来一个窗口,可以像其他View一样设置属性、透明度、变换等待)
    (step3:举个实践过的例子就很完美了)
    ----> 无论是SurfaceView还是TextureView底层都是Surface,如果对SurfaceFlinger很熟悉,可以引申,是加分项

本节回顾:

  • 探讨UI线程或主线程的定义
  • 分析Android进程的启动流程
  • 分析Looper的工作机制
  • 探讨为什么UI不设计成线程安全的
  • 给出UI线程的其他存在形式:GLThread和更新SurfaceView的Thread

2. Handler发送消息的delay可靠吗?

考察点:

  • 是否清楚UI时间相关的任务,如动画的设计实现原理(中级)
  • 是否对Looper的消息机制有深刻的理解(高级)
  • 是否做过UI过度绘制或其他消息机制的优化(高级)

题目剖析:
关键词:可靠

  • 答案肯定不可靠,但要深入分析原理
  • 给出基于原理的实践案例

主线程正常工作TODO:01:42
主线程亚历山大TODO:02:19
---> 线程堆积多了,就会卡顿
(对比下,很形象)
(step1:意识到问题)

错误的动画实现 TODO:04:37
调用时间并非delay的值,主线程比较卡的时候,会导致动画显示比较奇怪

MessageQueue如何处理消息 TODO:04:44
加入队列 MessageQueue.enqueueMessage():

  1. 如果消息在队列的最开始,并且符合执行的时间就马上处理
  2. 否则找到自己合适的位置插入进去
  3. 如果没有消息,就会进入阻塞态,等待消息
    处理队列 Message.next():单链表
  4. 外部for(;;)大死循环
  5. nativePollOnce(ptr,nextPollTimeoutMills)
    ---> nextPollTimeoutMills为-1,底层阻塞,因为刚才已经循环过了,发现mMessages已经没有消息了
  6. 如果不阻塞,就会去遍历,找到第一条能够执行的消息,如果马上执行就马上返回,如果不是,就设置nextPollTimeoutMills延迟等待
    (TODO:07:48)
    (step2:能把整个流程链条答出来,已经很满意了)

队列优化-重复消息过滤
---> 例如地图地图发送Render消息,要设置合适频率(太低也不行,用户也能感知加载不流畅)

队列优化-互斥消息取消
---> 例如地图发送Stop消息,前面的相关消息就没必要执行了

队列优化-复用消息
消息量比较大的时候,创建很多Message,就会频繁触发GC
--> 所以游戏开发一般不选择Java,因为它上面有个JVM,会GC,GC是stop the world的,直接C++自行管理内存了
---> Message.obtain(),利用消息池复用,使得消息数量大大减少,提升效率

消息空闲 IdleHandler
返回值:false表示一次性买卖,执行完就移除
---> Glide 3,利用IdleHandler移除GC调用图片的弱引用(ReferenceQueue可以监听到移除的事件)

使用独享Looper --> 主线程太挤了嘛
(Handler只能在主线程创建吗?其实不是,只有有Looper即可)
HandlerThread,可以传一个名字,线程有归属,你犯下的罪行就可以通过日志看出来,方便监控
(step3:了解到太体系化了)

用Handler实现动画的正确方法 TODO:19:09
(ValueAnimator、ObjectAnimator实现对实现动画很优化)

Handler发送消息的delay可靠吗?

  • 大于Handler Looper的周期时基本可靠(例如主线程 > 50ms)
  • Looper负载越高,任务越容易积压,进而导致卡顿
  • 不要用Handler的delay作为计时的依据(要求的精度很高的情况下) --> HandlerThread是相对靠谱的

本节回顾:

  • 阐述Handler的工作原理
  • 分析MessageQueue的消息处理机制
  • 探讨消息队列设计时的优化思路(这些消息是有关联的情况时)

3. 主线程的Looper为什么不会导致应用ANR?

考察点:

  • 是否了解ANR的产生条件
  • 是否对Android App的进程运行机制有深入理解(高级)
  • 是否对Looper的消息机制有深刻的理解(高级)
  • 是否对IO多路复用有一定的认识(高级)
    ---> nativePollOnce,实际上就用到了Linux的IO多路复用

题目剖析:
关键词:Looper、ANR

  • ANR是怎么产生的?
  • Looper的工作机制是什么的?
  • Looper不会导致应用ANR的本质原因是什么?
  • Looper为什么不会导致CPU占用率高?

ANR的类型

  • Service Timeout
    • 前台服务 20s
    • 后台服务 200s
  • BroadcaseQueue Timeout
    • 前台广播 10s
    • 后台广播 60s
  • ContentProivder Timeout:10s
  • InputDispacthing Timeout:5s

Service Timeout的产生

  1. 在启动Service的时候通过Handler发了个延迟消息(延迟时间根据前台还是后台设置) --> 埋下炸弹
  2. 如果在这个时间之前启动完成,会移除这个消息 --> 拆除炸弹
  3. 如果触发了这个消息,弹出ANR dialog
    (step1:知道ANR是什么东西,怎么产生的)
    ---> 其实ANR是耗时监控,实现方案闭环,因为对启动时间比较关注。

主线程究竟在干什么
ActivityThread的main函数进入Looper,一个循环处理消息
---> 回顾Handler原理

Looper和ANR的关系
Looper是一个整体线程上的概念,ANR是开发某个环境对开发者耗时情况的一个监控。
---> ANR是Looper里一个很小的子环节
(step2:知道Looper和ANR是什么关系)

---> 这个问题实质是坑,更应该知道的是
Looper为什么不会导致CPU占用率高
没有消息的时候,native底层是epoll_wait,等待文件的消息,本身会阻塞,阻塞的时候不会消耗CPU时间片,所以不会导致CPU占用率高

如果非常熟悉,可以展开说多路复用 TODO:13:50
不熟悉可参考《unix环境高级编程》,夯实基础
(step3:基础非常扎实)

本节回顾:

  • 分析ANR的类型以及产生过程
  • 探讨ANR与Looper的关系 --> 完全没关系
  • 分析Looper空闲时阻塞的原理

4. 如何自己实现一个简单的Handler-Looper框架?

考察点:

  • 是否对Looper的消息机制有深刻的理解(高级)
  • 是否对Java并发包中提供的队列有较为清楚的认识(高级)
  • 是否能够运用所学知识设计出一套类似的框架(高级)

题目剖析:
关键词:实现、简单

  • "简单"表明可以运用Java标准库当中的组件
  • 覆盖关键路径即可,突出重点
  • 分析Android为什么要单独实现一套
  • 仍然着眼于阐述Handler-Looper的原理

Handler的核心能力

  • 线程间通信
  • 延迟任务执行

<Android的Handler的delay用的是开启后的多少时间,所以设置延迟后,手动调快时间没有用>

Looper的核心能力
循环分发消息

MessageQueue的核心能力

  • 持有消息
  • 消息按时间排序(优先级)
  • 队列为空时阻塞读取
  • 头结点有延时可以定时阻塞
    ---> DelayQueue,如果不知道,按照Android单链表实现

Message的实现
因为使用了DelayQueue,所以要实现Delayed来比较优先级,本质上就是传一个延时的时间

HandlerThread
更完善一些 --> 可以直接抄HandlerThread

测试代码
很像ActivityThread

类结构 TODO:10:36
---> 没有实现remove
(DelayQueue不支持通过token移除)
(step1:能实现关键通路)

DelayQueue的阻塞机制
(step2:知道DelayQueue怎样实现延时,对比nativePollOnce),以及缺陷)

回顾Android的Looper对epoll的运用

Android为什么不直接复用DelayQueue

  • DelayQueue没有提供合适的remove机制 --> 结合堆想想很容易明白,移除是效率很低的
  • 更大的自由度,可以定制很多功能,特别是与Native层结合
  • Android的MQ可以针对单线程读取的场景做优化
    ---> DelayQueue很多地方直接加了锁,但实际上就一个Looper在访问,即读的时候一直只有Looper在读,只有往里面添加消息才涉及到多线程问题
    (step3:代码背后的逻辑,如果不知道,可以猜一猜,别管对不对,想的还是蛮多的,想的多的比较受欢迎)

本节回顾:

  • 给出最简版的 Handler - Looper实现
  • 分析DelayQueue的内部原理
  • 再次回顾Android Handler - Looper的运行机制
    ----> 理解机制,可以写一个简单的版本,覆盖关键路径,还容易注意到之前没注意到地方

第8章 不败之地:我当然做过内存优化【内存优化相关】

1. 如何避免OOM到产生?

考察点

  • 是否堆Java内存管理机制有一定认识(中级)
  • 是否堆Android内存有过优化经验(高级)
  • 是否在编写代码时有良好的习惯避免内存消耗(高级)

题目剖析:
关键词:OOM

  • OOM如何产生?
  • 如何优化程序减少内存占用?

OOM的产生

  • 已使用内存 + 新申请内存 > 可分配内存
  • OOM几乎覆盖所有的内存区域,通常指堆内存
  • Native Heap在屋里内存不够时也会抛OOM

使用合适的数据结构
例如HashMap和SparseArray(避免拆装箱)、ArrayMap TODO:02:46

小数组的复用池(小对象复用) --> 内存复用 TODO:05:37 <池化技术>
(减少GC,类似Message的obtain)

避免使用枚举 TODO:06:43

  • 枚举是个对象,占24个字节;int占4个字节
  • 一个简单的枚举编译出来会多1.0~1.4个KB的内存存在classes.dex

避免使用枚举,会有类型安全问题,所以引入了注解 --> IDE层级的提醒,无编译约束,Kotlin尚未支持

kotlin的解决方案:使用内联类(inline class)
编译时转为int,仅限Kotlin内部使用<1.3以后支持>
<kotlin字节码反编译一看,确实是成int了>
---> 多少会导致classes.dex增加

inline class Job(val value: Int) {
    companion object {
        val TEACHER = Job(0)
        val STUDENT = Job(1)
        val DOCTOR = Job(2)
    }

    fun setJob(job: Job) {}
}

Bitmap的使用

  • 尽量根据实际需求选择合适的分辨率
    --> 比如背景图片可以艺术范点、模糊点
  • 注意原始文件分辨率与内存缩放点结果
    --> 系统会根据配置的XXdpi进行缩放
  • 不使用帧动画,使用代码实现动画
    --> 代码很少,但图片非常吃内存
  • 考虑对Bitmap的重采样和复用配置
    ---> 重采样:比如缩略图,不要整张图片加进来;复用配置:复用内存,减少内存的开辟,减少GC
    (step1:进入了一个比较好的阶段,好的开始,只知道OOM怎么产生的是没有亮点的,很简单)

谨慎的使用多进程
一个进程fork出来以后就先天带有了一些公共的资源,系统预加载的,即使只有一行代码也会占用好几兆

谨慎的使用Large Heap
Java虚拟机:-Xmx4096m
Android虚拟机:android:largeHeap=“true” <ActivityManager.getLargeMemoryClass()>

  • 不同机型支持的不同
  • 大堆本身也会影响垃圾回收,因为它堆大了,回收速度肯要慢了
    ---> 用的好处么,不就是能够申请更大的内存吗

使用NDK

  • Native Heap没有专门的使用限制
  • 内存大户的核心逻辑主要在Native层
    • 各类基于Cocos2dx、Unity3D等框架的游戏
    • 游戏以外的OpenGL重度用户,例如各大地图App
      (可移植+避免Java的内存限制)
      (step2:已经考虑到很多的点)

---> 升华一下
内存优化5R法则
腾讯工程师胡凯总结的方法论:

  • Reduce缩减:降低图片分辨率/重采样/抽稀策略
    ---> 抽稀策略:类似地图的app,用点描述道路,可以减少点的个数来描述(感觉是算法相关)
  • Reuse复用:池化策略/避免频繁创建对象,减少GC压力
  • Recycle回收:主动销毁、结束,避免内存泄露/生命周期闭环
  • Refactor重构:更合适的数据结构/更合理的程序架构
  • Revalue重审:谨慎使用Large Heap/多进程/第三方框架
    ---> 需要熟悉源码
    (step3:方法论建设,高端意识)

推荐Android性能优化典范

本节回顾:

  • 简单分析OOM的产生机制
  • 从几个主要的方面探讨如何优化内存
  • 介绍业内专家给出的内存优化"5R法则"

技巧点拨:

  • 方法论建设 --> 高级工程师的一项重要指标

2. 如何对图片进行缓存?

考察点:

  • 是否对缓存淘汰算法有一定的研究(高级)
  • 是否对常见的图片加载框架有深入研究(高级)
  • 是否对算法效果有验证闭环的意识(高级)

题目剖析:
关键词:图片、缓存

  • 网络/磁盘/内存缓存
  • 缓存算法的设计 ---> 关键
  • 以熟悉的框架为例分析它的缓存机制
  • 要有验证算法效果的意思 ---> 优化一定要有量化

图片加载过程
大致都差不多,先看内存、再看disk、最后从网络上请求

缓存算法
考虑点:

  • 哪些应该保存?
  • 哪些应该丢弃?
  • 什么时候丢弃?
    评价缓存算法:
  • 获取成本:获取成本很高的话,缓存下来就很值
  • 缓存成本:缓存成本很高的话,比如很费内存,常用场景就不合适
  • 时间:随着时间的推移用的不多了,缓存价值就逐渐趋于0 了
    ---> 通过上述组合考虑 缓存价值 --> 通过通过命中率量化
    (step1:进入状态)

缓存算法
(Least Recently Used)LRU算法如果是权重相等的,最近使用的一直排到最后,溢出的时候,把最后的拿掉

(Least Frequently Used)LFU按照使用频率排序+使用时间<配合了LRU>,溢出的时候,用的最少的去掉
(step2:算法的细节能讲述)

LRU算法的实现

  • Android版本(v4)
  1. LinkedHashMap:
    参数:
    • initialCapacity:
    • loadFactor:
    • accessOrder:为true,访问LinkedHashMap里的元素后,该元素会放到这个链表的最后
  2. 统计监控:putCount、createCount、evictionCount、hitCount、missCount ---> 高端局的意识,20个字节的开销可以做到对LRU运行状态的了解
  3. sizeOf:默认是权重,默认是1,如果存的是Bitmap,图片大小就作为权重了
  4. get:小锁,没有加载方法上;加了两个短锁,只加在内部对象访问上,比如创建之类不涉及到LRUCache内部对象的访问(和put配合,Glide线程安全设计上,get是整个加了方法锁)
    ---> LRUCache设计出来,往往是需要多线程访问的,肯定是线程安全的
  5. trimSize:找到要移除的元素,android.util.LruCache里是最后一个元素,其实逻辑是错的,supportV4里是对的
    <看了下api27是对的,通过eldest()拿到头节点>
  • Glide版本
  1. 也LinkedHashMap,最近访问过的放到链表的最后
  2. put:相较于Android版本的get,没有create方法可供使用者自己实现缓存策略,避免自己实现的方法太长。简单粗暴的如果添加的元素超过最大限制,不添加
    ---> Glide的get就是简单的调用get方法
  3. trimToSize:和v4版本的一样,拿迭代器第一个

3. 如何计算图片占用内存的大小?

考察点:

  • 是否了解图片加载到内存的过程以及变换(高级)
  • 是否清楚如何对图片占用大小进行优化(高级)

题目剖析:
关键词:计算、内存
---> 不是运行之后直接去获取,而是给你一张图片,能直接知道放到什么文件夹,能占用多少,目的是提前设计程序

  • 注意是占用内存,不是文件大小
  • 可以运行时获取
  • 重要的是能掌握直接计算的方法

基础知识

mdpi hdpi xhdpi xxhdpi xxxhdpi
density 160 240 320 480 640
densityDpi 1 1.5 2 3 4

density是设备无关的抽象概念
---> 有大佬说过:任何问题都可以通过加一层来解决
(step1:这个思想,加分项)

运行时获取Bitmap大小的方法
Bitmap.getByteCount():图片应该占多大,理论需求之
Bitmap.getAllocationByteCount():图片实际上占多大
---> 因为有BitmapConfig,在实际使用的时候,读一张比较小的图片,但是可以复用之前已经开出来的内存,这个内存可能比当前图片理论需要内存的大

图片有哪些来源?

  • Assets中的资源等于文件系统
  • raw中的资源不经过任何处理
  • drawable目录系列注意dpi类型的影响

Assets中的图片
图片越大,文件肯定越大,但是数值上没有直接的关系
计算占用内存:
1.如果是ARGB_8888,是有4个通道的,每个像素需要4个字节 ==> 宽 * 高 * 4

  1. 如果是jpg,是没有Aplha通道的,默认是ARGB_8888格式,但实际上RGB_565即可(5+6+5=16,两个字节)
    ==> 宽 * 高 * 2
  2. RGB_565的png也是如此
  3. 如果是从XXXdpi里读,还和屏幕大小有关,图片的大小会发生改变:
    ===>图片 / 所对应的dpi的值 * 屏幕的密度
    (drawable为1,nodpi告诉系统不进行缩放,该多大多大)
    (step1:图片放哪了如指掌,且能计算内存大小,那就可以选择合适的路径存放了)

Drawable中的图片加载流程

  1. BitmapFactory.decodeResource(Resouse,Int,Options)
  2. decodeResourceStream()<此处可以看到drawable和nodpi的处理>
  3. 最终调到C++的BitmapFactory.doDecode()<采样 + scale>
  • 采样:原来宽高 / 抽样率
  • scale:+0.5f进行了四舍五入 --> 是不是有印象?

图片内存体积优化

  • 跟文件存储格式无关 --> 加进来默认都是ARGB_8888
  • 使用inSampleSize采样:大图 -> 小图
  • 使用矩阵变换来放大图片:小图 -> 大图(像素感没有那么明显)
    ---> 放大是交给绘制的,所以也是省内存的
  • 使用RGB_565来加载不透明图片
  • 使用9-patch图片做背景
    ---> 本身图片资源很小,对一些重复的资源可以直接拉伸,真正画出来的可能很大,但存起来的bitmap很小
  • 不使用图片 --> 比如帧动画,能程序实现就程序实现;还比如贝塞尔曲线,可以自己画
    • 优先使用VectotDrawable
    • 时间和技术允许的前提下使用代码编写动画
      (step3:满分答案,高话题固定套路:讲优化、讲性能、讲线程安全、讲架构、讲设计、讲意识、讲思想)

---> 扩充
索引模式(Indexed Color)
颜色的索引+索引表
--> 1px占1Byte,支持透明颜色,适合颜色较少的图片

  • 不能放入drawable类似需要缩放的目录中 --> 一旦缩放就变成了ARGB_8888
  • 得到的Bitmap不能用于创建Canvas --> Android的Canvas一般是ARGB_8888
  • 从Android8.1开始移除底层Indexed Color(Java层都有对应底层的,因为不支持,所以得到的为null)
    ---> 因为加个颜色,就得扩充索引表
    (如果只要支持比较低的版本,可能还有奇效)
    --> 知道这个,说明图片相关的知识相当雄厚(需要看很多底层源码)

本节回顾:

  • 通过现象解释各种情况下Bitmap内存占用的理论值的计算方法
  • 基于源码分析Drawable资源在加载过程中缩放的一些细节
  • 简单介绍一些图片优化相关经验和思路
  • 介绍Indexed Color以及Android对其"支持"的变化

第9章 不出所料:就知道你会问插件化和热修复【插件化和热修复相关】

1. 如何规避Android P对访问私有API的限制

考察点:

  • 是否能够熟练使用Java反射(中级)
  • 是否有Hook调用系统API的开发经验(高级)
  • 是否对底层源码有扎实的语言功底和较强的分析能力(高级)

题目剖析:
关键词:访问私有API、限制

  • 私有API包括哪些类型?
  • 如何访问私有API?
  • Android P如何做到对私有API访问的限制?
  • 如何规避这些限制?

私有API

  1. 通过注释@hide,写代码时android.jar里没有这个
    比如convertFromTranslucent() --> 右滑返回讲过
  • 自己打个带这个方法的jar包,系统里有的,运行时肯定没问题,除非系统把这个方法去掉了
  • 所以反射肯定也是能拿到的
  1. private修饰
  • 只能用反射

访问私有API

  • 自行编译系统源码,并导入项目工程(对public hide方法有效)
  • 使用反射:Accessible不仅可以绕过访问权限的控制,还可以修改final变量
    ---> 并不是把private修改为public,而是绕过了语言层面的控制
    (step1:万里长征第一步)

Android P的API名单 TODO:05:15
浅灰名单:反射可以用

---> 可以看出来,Android P是通过限制反射来控制的,问题一定在反射那

Android P对反射做了什么

(step2:知道问题产生在哪了)

第一个Hook点
GetActionFromAccessFlags找进去,发现一个hidden_api_policy

--> 基于第一个Hook点
开源框架FreeReflection原理剖析 <TODO:14:14 开始>
修改Runtime点hidden_api_policy
(step3:C的功底很全,大神级别)

第二个Hook点
fn_caller_is_trusted
---> 将调用者的ClassLoader置空
(仅限于这个类,方案难度较大)

第三个Hook点
<runtime->GetHiddenApiExcemptions()>
前面虽然禁止了,但是后面豁免了 ---> 赶上大赦了

2. 如何实现换肤功能?

  • 是否了解Android的资源加载流程(高级)
  • 是否对各种换肤方案有深入的研究和分析(高级)
  • 可借机引入插件化、热修复相关的话题(高级)

题目剖析:
关键词:换肤

  • 主题切换
  • 资源加载
  • 热加载还是冷加载
  • 支持哪些类型的资源
  • 是否支持增量加载

系统的换肤支持-Theme

  • 只支持替换主题中配置的属性值
  • 资源中需要主动引用这些属性 --> 必须用attr,如果用color就直接加载颜色了
  • 无法实现主题外部加载、动态下载
    ---> 太局限了

资源加载流程 TODO:03:03
常用的:getDrawable/getColor/getString,以getDrawable为例

  1. 通过context.getDrawable,调用到Resource的getDrawable
  2. 根据是xml还是非xml,让AssetManager选择加载的方式

而context.obtainStyledAttributes调用Theme的obtainStyledAttributes,兜兜转转调用到AssetManager到applyStype

AssetManager.openAsset是开发者手动调的
(step1:对资源加载很熟)

资源缓存替换流
因为Resources里有些固定的字段
(sPreloadedDrawables/sPreloadedColorDrawables/sPreloadedComplexColors)
虽然随着版本不同,名字可能不一样,但总的来说还是有这些东西的,是加载资源进来的缓存

  1. 原先流程应该是走AssetManager拿资源的
  2. 但是我们可以预先把它从从Skin Resources里加载进来
  3. 所以就偷梁换柱了,Skin Resources里没有的时候,才会去AssetManager里找

Resources包装流

  1. 原先是通过调用Resources找资源的,但我们可以在这之前加一层ResourcesWrapper
  2. 让getDrawable/getColor/getText都先走ResourcesWrapper去找Skin Resources
  3. 所以如果Skin Resources里有资源,那就加载皮肤资源的,没有的按正常流程加载

AssetManager替换流
从根源上解决,所有经过AssetManager都可以改了,这个方案稍微厉害点。

  1. Native AssetManager里有mAssetPaths,可以有很多皮肤资源包,也包括系统的资源包
  2. 所以可以通过反射添加自己的资源包

方案对比 TODO:06:53

缓存替换流 Resource包装流 AssetManager替换流
工作机制 修改Resource的字段 包装Resources拦截资源加载 AssetPath中添加皮肤包
刷新方式 重绘View 重绘View 如替换布局,需重启Activity
方案优势 支持图片资源;支持独立打包动态下发 +支持String/Layout +支持style;+支持assets目录下的文件;+替换AM实例非常简洁
存在问题 替换资源受限;Hook过程较为繁琐;影响资源加载,入侵性较强 资源获取效率有影响;不支持Style、assets目录;Resource需求替换多处;Resource包装类代码量大 5.0以前不能新增Entry;强依赖编译期资源id的一致性处理
资源重定向 无此问题 运行时动态映射;编译期静态对齐(可选) 编译期静态对齐
  • 缓存替换流大量的使用了Hook
  • Resource包装流不能替换Style、assets目录,是因为这些是AssetManager直接去读的
  • AssetManager是本身支持的,入侵性最小,替换一个AssetManager实例即可
    (step2:思路说出来,已经有一定认可度)

----> 追究细节
资源重定向:默认编译生成的id一般来说是不会相同的,常量整型替换引用

动态映射方案

  1. 先拿到id的值
  2. 通过id可以找到名字,比如id/button
  3. 把Package从主包换成皮肤包的,通过名字映射回来,找到对应的正确的button的值

静态编译方案

  1. AAPT编译资源时输入主包的id映射,public.xml
  2. 编译后根据主包映射关系修改皮肤包的resources.arsc

资源增量静态对齐
对于attr,加载有些特点

  1. 假设主包里有3个Entry,皮肤包里有2个Entry
  2. 因为编译的时候会去检查Entry的个数,attr是按顺序排下来的 --> 也是为了让获取更有效
  3. 这时比如想要读取attr2,比皮肤包的entry个数大,那说明皮肤包里没有
  4. 也就不会继续走下去了,但我们希望如果皮肤包中不存在,读取主包的资源
  5. 所以需要给皮肤包没有的资源用空值强制占位,cheat一下,这样在皮肤包里找不到就会去主包找了

另外:

  1. R.attr.attr1皮肤包中为定义,编译时AAPT会报错
  2. 若剔除public.xml的R.attr.attr1,编译时后续非public的资源会顺序占坑
    ---> 也是为了保持资源的紧凑
    这样比如找attr1,对应皮肤包的就不对了,整个就乱掉了
    解决:定制apt或者修改资源包,让它支持没有资源的占坑,皮肤包里不存在就可以找到主包里的了

定制AAPT实现占坑
ResourceTable::applyPublicEntryOrder
上述的 找不到占坑 + 最后没有了占坑
(更改AAPT和后期维护不容易)

---> 运行时增量替换是相对麻烦的
简单方案:皮肤包资源增量差分方案

  1. 通过主包和皮肤包的差异,差出来一个差分包
  2. 客户端拿到主包和差分包,合成一个完整的皮肤包,运行时直接替换掉
    ---> 替换新的AssetManager时,只需要添加一个AssetPath
    问题:
  • 重定向问题还是会有
  • 因为皮肤包有所有的资源,加载到内存里会大一些
  • 合成的时候也有一定开销,如果皮肤包比较大,会比较耗时

AssetManager替换流的实现

  1. 反射拿到addAssetPath方法
  2. 加载方式版本不同有些差异:
  • 4.0先添加主包的资源,再添加皮肤包
  • 5.0拿到皮肤包后需要先进行拆分,把asset里的文件单独拿出来,先加载皮肤包
    再加载主包、最后加载Asset
    ---> 原因是加载顺序:AssetFile读的时候,优先读最后加进来的资源包,跟资源不一样。4.0和5.0加载顺序是反的,但AssetFile和添加顺序正好是一致的
    <我们目的是优先读皮肤包>
    (不太理解)
  1. 替换AssetManager(有很多种),比如可以通过包装Context,因为ContextWrapper里持有AssetManager
    ---> 为什么要解包装?因为在ActivityThread里,在一个收广播(印象中)地方,会判断类型,如果不是ContextImpl就会抛异常了
  • 如何让每个Activity覆写attachBaseContext和getBaseContext?
  1. 都继承自BaseActivity --> 但会改变继承结构(试试代理?)
  2. 通过Javasist修改字节码完成自动注入(例如RePlugin)

换肤和插件化是有差异的

  1. 换肤框架要保证资源id不变,是覆盖关系
  2. 插件化框架资源id不同,是并存关系
  3. 插件化框架宿主资源共享不存在覆盖
    (step3:有实践经验,分析的很详细,源码看了不少)

本节回顾:

  • 探讨常见的几种换肤框架的实现思路
  • 对比资源缓存、Resource、AssetManager替换方案(递进越彻底,AssetManager方式已经很接近插件化了)
  • 探讨资源重定向的两种不同的解决方案
  • 给出AssetManager替换方案的核心实现

3. VirtualApk如何实现插件化?

考察点:

  • 是否清楚插件化框架如何实现插件APK的类加载(高级)
  • 是否清楚插件化框架如何实现插件APK的资源加载(高级)
  • 是否清楚插件化框架如何实现对四大组件的支持(高级)

题目剖析:
关键词:插件化

  • 不一定讲VirtualApk,说你熟悉的
  • 如何处理类加载
  • 如何处理资源加载和冲突
  • 如何对四大组件进行支持

VirtualAPk
VirtualApk是运行在Android上面的一个Apk,有一个宿主,本身是个Apk,这个Apk有加载其他插件Apk的能力。
(VirtualApk插件之间不是完全隔离的,完全隔离的是DroidRlugin)
---> 业务相关了,VirtualAPK是滴滴的,比如各个打车插件都需要依赖地图插件那就要依赖宿主了

插件化方案对比

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
支持四大组件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持
组件无需在宿主manifest中预注册
插件可以依赖宿主
支持PendingIntent
Android特性支持 大部分 大部分 大部分 几乎全部 几乎全部
兼容性适配 一般 一般 中等
插件构建 部署aapt Gradle插件 Gradle插件

如何加载运行插件代码

  1. LoadedPlugin持有Apk信息,类似系统的LoadedApk持有Apk信息
  2. 加载插件代码方式有两种:
    • 如果是Combine_ClassLoader,需要加载到宿主里面(不需要隔离)
    • 如果不是,就不用加载到宿主里,实现隔离
      ---> 需要注意的是,创建的DexClassLoader的parent是宿主的ClassLoader<PathClassLoader>,所以实际上隔离不隔离都可以用反射拿到宿主里的类的(那为何要特地插入呢?)
      对应映射图(TODO:06:55)
  3. 插入宿主的原理:把dex插到后面,插入完以后就宿主里的ClassLoader就成了巨无霸了,既能加载宿主里的类,又能加载插件里的类
    ---> 与QQ空间超级补丁的热更新不同,超级补丁插到最前面,优先加载 ,毕竟不是为了修复

对比:DroidPlugin的超强隔离
把插件的DexClassLoader和宿主的PathClassLoader,位与双亲委派的同级,且插件之间是又是不可见的,所以完全隔离
(step1:万里长城第一步,解决了类)

如何处理插件资源
和加载类一样,分为两种模式 ---> 和滴滴的业务很有关系

  1. 如果是Combine_Resource,加载宿主和插件的资源(不需要隔离)
  2. 如果不是,只加载插件的资源

资源编译处理及过滤 --> 没有combine,只有Plugin,加载起来什复杂的地方
和换肤还是有差别的,换肤需要id来映射,但插件化不需要,可以通过编译的时候过滤掉重复的id
--> 在编译的时候可以更改(把没有重复的资源,用他的包的标识来确保不重复)
- 注意R文件也要改,相当于重定向了
- apt编译,资源表是顺沿的,保持连续

插件和宿主资源没有重复(编译过滤)
插件资源id的package被修改

---> 引申
资源过滤存在的问题
Q1:插件开发和宿主开发都是独立的,如果有一方修改了对比用的资源表(id不同了),虽然插件编译可以成功,但是运行的时候通过这个id,肯定是找不到了的(因为都乱套了)
---> 为什么是插件里找不到?想想ClassLoader是先加载宿主的,所以插件的R.java不会被加载
解决:

  1. 把插件的R文件的final去掉,编译期间就不会用插件的R来替换了,所以用的一直是宿主的R文件
    ---> 需要了解类加载和编译期常量才能想到
    缺陷:没办法解决xml里的引用,因为编译完以后,由apt直接替换了
    ---> 加载资源的时候去Hook(比较麻烦了);或者宿主不变,利用public.xml,但是资源是按顺序排的,所以这个文件只能增加,不能删;
    (这个方案可以作为只有Java代码的方式的候补方案)

Q2:比如app_name/about类似的资源,宿主和插件定义的名字可能重复
(资源名称相同,资源本身不同)

(step2:能把资源加载的细节给讲清楚,面试体验会非常好,给面试官感觉像是学术交流)

如何支持启动插件Activity
熟悉Activity启动流程中知道,AMS不知道插件里面有哪些组件,因为需要解析Manifest注册的,插件里并没有注册。
解决:通过占坑的Activity,欺上瞒下

  1. 启动过程中,拦截到这个启动插件Activity的请求(第一个Hook点) --> 替换Intent
  2. 告诉AMS启动占坑的Activity
  3. 回到自己的进程,通过Instrumentation或者其他的类拦截,来启动插件的Activity(第二个Hook点) --> 替换Activity

启动插件Activity的问题 --> 看看还有没有问题
VAInstrumentation.handleMessage,设置了ClassLoader是宿主的PathClassLoader,这个ClassLoader可能有插件的ClassLoader,也可能没有,没有的时候就会出问题
---> 因为加载不了插件的类,肯定会加载失败了(AMS返回的时候判断是否是插件来选择反序列化的时候就有问题了 --> Bundle在解extra的时候会全部解析,反序列化,所以肯定是有问题的)

<不将插件ClassLoader注入到宿主ClassLoader时有反序列化问题>
----> 使用过程中,得保证是combine的,让他有插件的ClassLoader
TODO:验证下

对比:DroidPlugin如何处理此问题
Intent包装启动插件的Intent,插件的Intent作为新的Intent的extra,回到App进程,拿到宿主的Intent再判断是哪个插件,无需提前反序列化了
--> 实现原理是Intent实现了Pracelable
(step3:满分答案)

如何支持启动插件Service
根据进程分为LocalService和RemoteService:

  1. 动态代理替换AMP(AMS在客户端的代理)
  2. 发送前包装Intent,即把启动Service的Intent作为extra
  3. LocalService代理目标服务,通过反射拿到需要启动的Service,指向对应操作
    <启动Service的时候就包装了,没有Activity的反序列化问题了>
    ---> 和DroidPlugin很像 (看下当前版本的VirtualAPK)

如何支持注册广播

  • 解析插件Manifest,静态广播转动态注册
  • 插件广播在宿主未运行时无法被外部唤醒
    ---> 因为变成动态广播了,AMS不知道,一旦宿主挂了,就没办法被拉起来了,所以需要保活的需要放在宿主里
  • 系统限制只能静态注册的广播可在宿主预埋并处理
    ---> 比如需要开机自启的

如何支持注册插件ContentProvider
---> 实现思路类似,自行阅读源码

本节回顾:

  • 分析VA如何支持插件类加载
    • 对比DroidPlugin的类完全隔离方案
  • 分析VA如何支持插件资源加载
  • 探讨VA资源过滤处理的使用场景以及问题
  • 分析VA如何支持插件Activity的启动
    • 探讨Intent的Extras反序列化的问题
  • 分析VA如何支持插件Service的启动

4. Tinker如何实现热修复?

考察点:

  • 是否有过热修复的实战经验?(中级)
  • 是否清楚热修复方案如何对代码进行更新?(高级)
  • 是否清楚热修复方案如何对资源进行更新?(高级)
  • 是否具备框架设计开发的技术功底和技术素养?(高级)

题目剖析:
关键词:热修复

  • 不一定讲Tinker,说你熟悉的
  • 如何支持代码的热修复
  • 如何支持资源的热修复

Tinker工作流程

  1. 修复后的APK和基准APK,差异出patch.zip
  2. 把patch.zip下发到用户,组合成修复的Apk
  3. 工作启动的还是基准包,把修复完成的Apk的Dex放到基准包的Dex前面,那ClassLoader就可以优先加载了。资源的修复比较简单,直接替换掉就行了。 --> 所以后面下发的,前面的就失效了
    (step1:知道基本工作流程,是了解原理的基础)

对比 QQ空间超级补丁:如果只包含修复的类,如果这个类被其他的类引用,这个类会报is_pre_verified的异常

---> 因为Tinker是整个的dex,所以会很大
Java代码修复 - 基于Dex的差分算法
DexSectionDiffAlgorithm ---> 难点在于这个数据结果
DexSectionDiffAlgorithm.execute():
先排序,再比较,通过两个分别指向old和new指针

  1. 只剩新包的元素,一定是Add的
  2. 只剩基准包的元素,一定是Delete的
  3. 中间的需要比较,old<new一定是删除的,old>new一定是新增的,old==new,没变,但需要记录位置和offset(offset也是为了优化)
  4. 连续相同index的Add和del,替换为replace --> 优化(微信开源的东西是做到极致的)

Java代码热修复 - 基于Dex的合成算法
DexSectionPatchAlgorithm --> 相对于差分比较简单

Dex加载
把extraElements,优先加载修复过后的dex

回顾:皮肤薄资源增量差分方案
Tinker选择把修复包和基准包,差分出一个差分包,下发到客户端的时候去合成资源包,运行时加载这个完整的资源包(更容易维护,但资源包比较大的时候会比较耗时)--> 不然需要定制apt生成,虽然可以减少合成的开销

资源热修复 - 基于Entry的BSDiff
基于Entry,粒度更细,生成的Patch包会更小(只是细微改动的话,生成diff就很小) 还一份,修改过的资源包,方便我们合成 --> 因为需要下载的

资源加载
类似AssetManager流换肤,利用AssetManager把这个资源包加载进来(需要兼容5.0,assetFile)
(step2:Tinker的细节都很了解,尤其是差分算法)

---> Tinker很极致
细致的异常处理

  1. 校验MD5
  2. 统计耗时
  3. 失败回滚 --> 卸载热修复的包
    异常熔断

监控&闭环意识
Tinker的监控代码埋的很多,平均一两百行就有一处监控,早期版本就说有129处

良好的注释
Tinker的注释量也很多,占比20%
(step:代码规范和把控很大)

本节回顾:

  1. 探讨Tinker的工作机制
  2. 分析Tinker DexDiff算法的思路
  3. 探讨Tinker的Dex加载机制
  4. 探讨Tinker的资源热修复机制
  5. 探讨Tinker项目提现出的其他优秀品质 ---> Tinker很极致,教科书式代码

第10章 不离不弃:我做事情一向追求极致【优化相关】

1. 如何开展优化类工作?

考察点:

  • 是否对项目整体目标有清晰的认识(高级) --> 初级工程师被安排,高级工程师考虑合理性并进行拆解分配工作
  • 是否能对项目的重点问题进行拆解(高级)
  • 是否有追求极致的技术功底和主观意愿(高级) --> 能力是否达到,技术很强却是得过且过
  • 是否能够在关键时刻承担有挑战的工作(高级)

题目剖析:
关键词:优化类

  • 通常作为大项目的重点专项存在(路径闭环了,细节影响项目开展,高管可能都会盯着你)
  • 是具有系统性、全局性的局部工作
  • 更能凸显你追求极致的精神

明确优化的目标:

  • 耗电量优化?
  • 过度绘制优化?
  • 内存优化?
  • CPU占用率优化?
  • 算法策略优化?

定性到定量的改变:

  • 定性:咱们的App耗电量太高了,需要优化!
  • 定量:后台运行10%/小时,目标3%/小时
    ---> 体现你能确立清晰的优化目标,而不是想当然的开展优化

定位关键问题:

  • 优先解决占比最高的问题
    二八定律:80%的错误通常源自于20%的问题
    • 优化前期花20%的精力就能解决80%的问题(前期很有成就感)
    • 优化后期相反
      ---> 实际工作可能会有冲突,但是不能说的太low,目的是为了凸显自己很强

业内横向对比:
比如做插件化,VirtualAPK 和 RePlugin 进行比较
---> 体现考虑问题比较全面,而不是盲目造轮子

完善指标监控:知道优化效果

线上灰度:虽然在测试上或者QA验证过,但是不可能过了所有case,不要太过于自信,要慢慢的上线,全量可能会造成灾难。

项目收益:

  • 转换成面试官有概念的指标
    • 页面加载时间减少800ms
    • 内存消耗降低50MB
    • CPU占用率由12%降低至3%
    • 项目成本由8元/单降至3元/单(平均10000单/天)

人力优化

  • 主要针对项目负责人
  • 需要堆项目成员的能力有足够的了解
  • 需要对项目功能做合理的拆解
  • 用合适的人做合适的事
  • 适当放权,但也要依据情况做好辅导

优化心法

  • 深入钻研技术为优化提供可能性
  • 结合业务场景为优化提供落脚点
  • 熟悉团队特点为优化提供战斗力

比如算法优化:

  1. 如果涉及大量的矩阵运算,Java层运算会导致频繁GC,可以考虑小矩阵池化,减少对象的频繁创建和频繁GC
  2. 如果JNI调用频次很高,可以考虑C重写,而且直接使用物理内存,减少GC

比如业务优化:(比如入库耗时)

  1. 加密结果较大是源数据较大导致,在探讨后,不影响业务的前提下优化了数据格式,将JSON格式的源数据改为Protobuf进行加密,源数据减少了60% ---> 项目中尝试下
  2. 源数据存储与sqlite,实验发现其二进制读写性能不如文件直接读写,因此同样不影响数据的情况下直接从文件系统读取,性能提升约5%
  3. 将算法做了优化,确保安全性的前提下,由原先的全文件加密,改为局部加密,文件不需要完整加载和回写,直接随机读写文件系统就可以解决,IO耗时减少了90%(还顺便进行了内存优化,因为读出来的也很大)

简历上(公司内部晋升)避免很模糊,自己还没想明白,很容易被挑战

本节回顾:

  • 探讨了优化类项目开展和阐述的关键思路
    • 量化指标,完成从定性到定量的转变
    • 定位问题,二八定律与关键性问题的解决
    • 横向对比,避免遭到为什么自己造轮子的挑战
    • 完善监控,优化效果有据可查
    • 项目收益,给出听得懂的收益指标
    • 人力优化,合适的活给合适的人
  • 给出一个不好的例子并给出改进示例

2. 一个算法策略的优化case

---> 算法策略一般是和业务紧密结合的,讲太具体也不太愿意听
算法策略

  • 结果确定性:主要是性能优化 --> 比如排序这些
  • 结果模糊性:受样本影响大,算法本身的优化

优化前的项目状况

  • 指标:XXX准确率,例如语音识别率
  • 量化方案:无,主要凭感觉
  • 策略验证方案:无

量化指标

  • 给出XXX准确率的数学定义和计算方法
  • 简历指标获取、策略验证的流程和方法
  • 搜索建立充足的样本集得到指标的现状
  • 确定合理的KPI,例如从78%优化到92%
    (step1:指标量化后,对算法类已经是很大一步了,通常指标获取没那么容易)

对比现有技术方案

  • 阅读相关学术论文
  • 与有相关经验的团队进行技术交流

问题分析
不同角度看问题,占比不同,重点突破关键问题 --> 二八原则
---> 有余力要全面突破

监控体系建设

  • 针对算法效果指标XXX准确率做监控
  • 根据项目特征确定指标汇报频率
  • 定期发送线上运营数据报表,展现项目效果
    (step2:一个团队需要的考虑的点)

算法策略动态下发
算法迭代 --> 应用发版 --> 用户更新 --> 算法生效
变更为
算法迭代 --> 动态下发 --> 算法生效
(因为算法往往是平台无关的,可以考虑插件化和脚本化)
---> 应用效率提高,是对优化的优化

工具完善
人工 --> 自动指标量化 --> 辅助问题定位

灰度上线 TODO:10:12
--> 显得思考问题很成熟

直接指标量化

  • 直接指标变化
    • 比如语音识别准确率从80%提升到90%
  • 结合业务场景给出宏观收益
    • 比如提高了识别率使得项目成本节省了多少钱
    • 广告精准投放使得广告收入每天增加多少钱
      (step3:一环扣一环,天衣无缝)

本节回顾:

  • 指标量化:确定计算方法、指定目标
  • 问题定位:分析问题、方案对比
  • 问题解决:解决80%的问题、追求极致
  • 项目收益:直接收益,宏观收益

3. 一个工程技术的优化Case

项目背景:一个视频截图sdk的效率优化工作

项目收益:设备覆盖面扩大

开源框架的License --> FFMpeg的Hall Of Shame

本节回顾:

  • 现有方案介绍
  • 工程流程介绍以及关键问题分析
  • 纯技术优化点
    • TS快速寻址
  • 结合业务的优化点
    • I帧近似
    • 编解码流程优化 ---> 大多数优化是业务优化了,一直是技术优化,视野是比较低的了
    • FFmpeg License

<面试官目的是为了发现你过去怎么做的,你能做出什么,来猜测将来能做怎么样的工作,能有什么样的成果>

第11章 不同凡响:拆解需求设计架构是我常做的事儿【架构设计相关】

1. 如何解答系统设计类问题?

---> 对于基础比较好的,非常友好的问题
考察点:

  • 是否能够快速理解需求并对需求进行拆解(中级)
  • 是否具备广泛的技术栈或知识面(高级)
  • 是否能够深入挖掘需求给出良好的技术方案(高级)
  • 是否具备良好的项目管理和领导能力(高级)

题目剖析:
关键词:系统设计类

  • 解答过程中与面试官要保持良好的沟通
  • 如果系统足够大,则不需要解释太多细节
  • 如果系统较小,最好辅以精妙的细节设计

项目诞生记:
提出想法 --> 可行性研究 --> 需求分析 --> 系统设计 --> 系统开发 --> 迭代维护 --> 系统重审

面试官:提出想法
候选人:需求分析、系统设计

系统设计步骤:

  1. 需求:设计(项目需求)一个网络请求框架
  2. 关键流程:关键就是打包请求、建立连接、发送请求、解析结果
  3. 细节:请求和响应数据结构适配能力(Adapter)、请求重试机制(拒绝策略)、异步处理能力、使用体验优化

回顾:如何用Java实现Handler

  1. 需求:移植Android Handler 到 Java平台
  2. 流程:关键消息队列、死循环、阻塞和延时
  3. 细节:是否需要支持底层、消息队列性能优化、消息实例池化

系统设计三步走:
明确边界 --> 打通流程 --> 优化细节

常见细节:

  1. 如何处理好并发?
    • 是否有频繁的IO操作?
    • 线程调度如何设计?(线程池使用限制、线程数?)
    • 业务操作中异步程序如何设计?
      • RxJava
      • 协程(Kotlin)
  2. 网络如何接入?
    • 是否需要频繁与服务端交互?(根据需求是否短连接即可,很频繁考虑多拉些数据、连接池化)
    • 是否存在服务端主动推送消息的场景?
    • 采取何种通信手段?
      • 长连接:高频交互,消息推送,维护复杂
      • 短连接:低频交互,消息推送(短轮询、长轮询)

短轮询:每隔一段时间请求服务端;
长轮询:发一个请求,服务端不返回,直到有消息再返回,一直没消息,比如经过60秒,客户端可以重新发起请求。
相比之下:短轮询有延时问题,长轮询延时比较小,维护成本相对于长连接开销比较小

  1. 保障安全性

    • 数据是否需要加密?(避免被竞品爬走)
    • 加密算法如何选择?(需要业务和体验的结合,视频每个字节加密,解密就很卡了)
      • 对称加密:密钥如何保存?
      • 非对称加密:注意加密复杂度限制(耗时长)
        ---> 通常用对称加密对数据进行加密,用非对称加密对对称加密对密钥进行加密(https好像就是)
    • 应用安全性如何保证?
      ---> 危害:被破解,植入广告(混淆、加固、验签)
  2. 热修复与插件化
    热修复一般都需要,关键看方案选型

  • 是否要求立即生效? --> 有些app是开机一直运行的
  • 是否要求新增或修改类?<这个有不需要多情况吗>

插件化主要考虑体量

  • 前期通常不需要插件化,但可未雨绸缪
  • 是否融合了多条业务线,多团队协作?
  1. 脚本化
    <二八定律:80%的版本都是20%的代码需要修改>
  • 是否存在大量可模式化的逻辑?
    1. 游戏关卡
    2. 自定义的UI体系
  • 是否存在大量需要经常调整的策略? --> 经常调整
    1. 简单的参数调整无法满足
  1. 可移植性:
  • 是否存在平台不相关逻辑?
    1. 如语音识别、OpenGL绘制逻辑
    2. 考虑C++开发(可以在win上跑,便于调试)
  • 是否考虑移植UI?
    1. Flutter/React Native
      (常用套路:上层UI用flutter,下层业务逻辑选择平台无关的语言,比如kotlin native)
  1. 性能问题:
  • 算法复杂度和时间复杂度?
  • 内存峰值是否偏大有无OOM可能?(图片多吗?)
  • CPU占用率是否持续较高?(视频播放->软解码,硬解码是否支持该格式?)
  • 耗电量是否高居不下(是否一直需要屏幕亮着)
    TODO:常见操作的耗时 --> 可以心里有数,规避一些瓶颈的要点
  1. 监控:
    --> 高级工程师意识的体现,要注意反馈,形成一个闭环
  • 异常捕获以及状态保存恢复
    • Java层异常捕获
    • Native层异常捕获 --> 比如视频录制功能,Native挂了肯定读不了了
  • 性能监控
  • 优化指标监控
  • 运营数据监控

思考过程:

  • 系统设计题没有标准答案
  • 深思熟虑地选择技术方案
  • 展示你的知识深度和广度
  • 思考过程比最终结果重要

本节回顾:
三个步骤:明确需求、打通流程、优化细节
十个方面:

  • 并发网络与安全,脚本热修复插件
  • 性能监控可移植,思考过程是重点

2. 设计一个短视频App

---> 通常和业务背景有关
考察点:

  • 是否对短视频乃至视频行业业务有认识(中级)
  • 是否有丰富的系统设计架构经验(高级)
  • 是否对音视频相关技术领域有一定的积累(高级)

题目剖析:
关键词:短视频APP

  • 视频处理一定是重点
  • App设计除业务本身外其余大多想通

明确需求边界

  • 视频来源自有服务还是第三方?
  • 视频由用户上传还是专业供应平台提供?
  • 是否需要建立用户关系链?
  • 是否需要支持视频分享?
  • 是否需要建立支付系统方便打赏?

打通关键流程 TODO:04:30 (抖音的Feed流就全是视频了)
发布者:

  • 录制视频
  • 上传视频
    订阅者:
  • 下载视频
  • 播放视频

播放器比较专业化了,相机两套API需要兼容
(step1:知道需要做哪些东西)

播放器可移植
一个是需要支持iOS和Android,另一个是平台级的App肯定需要共用一个播放引擎,减少开发的人力成本,所以播放器肯定要用做到平台不相关的

滤镜脚本化
比如如果用OpenGL,着色器的脚本是文本格式的,目的是为了动态下发,不需要app发版即可支持很多滤镜
(Lottie目的也是这个,支持多种动画,也是脚本化) --> 研究研究

安全性

  • 视频文件安全性,防止竞品非法获取 --> 法律手段滞后,会错失商业机会
    • 加密耗时影响体验,注意加密算法选取
  • 应用做好混淆和加固,防止篡改植入广告
    (step2:知道核心的点,有自己的思考)

----> 需要有想关经验
成本优化

指标 H.264 H.265
硬件支持 几乎全部 很少
文件大小 1 0.5
编解码耗时(硬件) 1 3-7
  1. 针对热点视频采用H.265 --> 很少用了H.265,稍微慢一点用户无感知,但是成本减少了
  2. 针对性能较好的机型动态切换软解H.265与硬解H.264

播放优化

  1. 根据MP4文件的格式内容:fytp,moov(索引之类)、mdat(数据)
    [ftyp-mdat-moov]会导致无法边下边播,索引上传了这样格式的,在服务器进行转码,首先收到moov,通常不大
  2. 播放器行为限制:iOS/Android 7.0以上需要等到一个GOP(一组图像)的时候才能播放,Android 6.0以下,要收到5秒视频数据才能开始播放
    优化:基于FFmpeg自研播放器,收到关键帧就可以播放了,既可以实现抖音一样的秒播

流量合作

  • 专属流量,降低用户使用成本
    (step3:业务和技术都很强)

本节回顾

  • 设计一个短视频App
    • 分析需求,确定系统涉及的业务方向
    • 打通流程,抓住关键的视频上传下载播放等环节
    • 优化细节,在成本控制、体验优化上下功夫

3. 设计一个网络请求框架

(麻雀虽小,五脏俱全)
考察点:

  • 是否具备扎实的网络通信基础(中级)
  • 是否有丰富的网络开发经验及需求细化能力(高级)
  • 是否具备通用基础框架的架构设计能力(高级)
  • 是否有框架使用体验优化的意识和思路(高级)

题目剖析:
关键词:网络、框架

  • 所有跟网络相关的,不要局限于Http
  • 框架设计的几点注意事项
    • 依赖关系尽可能简单
    • 对外接口尽可能易用
    • 功能设计尽可能纯粹

明确需求边界

  • 单向请求还是双向请求?
  • 需要支持哪些应用层协议?
  • 是否需要支持自定义协议扩展?
  • 是否需要支持异步能力?
  • 运行在什么平台上(可移植)?

打通关键流程
协议层:Http、WebSocket
基础组件:连接管理、线程管理

Tips:关键模块可绘制UML图:TODO:02:50
Connection:write/read/close
ConnectionManager:create(url,reuse)<复用能力>
(step1:进入状态)

为Http协议添加缓存机制
存储位置:内存/磁盘
淘汰策略:默认采用LRU算法
接口开放:全局开启或禁用缓存/策略、参数可配置
(逐渐外层)

增加全局数据拦截
Servlet、OkHttp都有

  1. 修改请求:全局参数,登陆状态信息
  2. 处理共用的结果返回
  3. 模拟服务能力
  4. 日志工具:打印结果,方便调试

重试机制
<拒绝策略>

  • 可设置最大的重试次数
  • 可指定频率衰减因子
    (step2:对网络请求流程和需要的东西有所了解)

使用注解配置请求
协议是很模版化的东西,写着很累 --> 很多框架都采用注解简化配置,例如Spring、Retrofit

第三方扩展
比如支持Kotlin和RxJava(比如Retrofit)

代码设计模式

  • 协议体构建使用Builder模式
  • 数据的传输与拦截使用责任链模式
  • 数据序列化类型支持使用适配器模式(+泛型应用)

主要设计的高级语法

  • 注解:主要用于接口配置和参数解析
  • 泛型:主要用于数据类型的适配
  • 反射:读取注解信息、反序列化类型等等(配合前二者)

DNS增强
<默认是通常是去运营商查询,有可能返回的是一个被劫持的,比如植入广告的ip> --> 第三方DNS就这么容易被劫持吗?
让网络框架支持DNS查询,如果自己公司有的话,请求自己公司的,一般是拿到一串IP,一个不成功请求下一个,如果没有可以找一些大的厂商,一般都会有的(比如阿里、腾讯)。
(step3:思考的蛮多的,没遇到过劫持一般想不到)

本节回顾:

  • 设计一个网络请求框架
    • 分析需求,确定系统功能边界
    • 打通流程,抓住关键网络请求流程
    • 优化细节,拦截器、第三方支持等等

(三大步骤,十个方面想一想,很有可能思如泉涌)

第12章 课程总结

题目本身不重要,关键是回答的思路,触类旁通

要不断和面试官沟通,面试官要看候选人的表达能力,沟通能力

相关文章

网友评论

    本文标题:草稿-大厂资深面试官 带你破解Android高级面试(part2

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