第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():
- 如果消息在队列的最开始,并且符合执行的时间就马上处理
- 否则找到自己合适的位置插入进去
- 如果没有消息,就会进入阻塞态,等待消息
处理队列 Message.next():单链表 - 外部for(;;)大死循环
- nativePollOnce(ptr,nextPollTimeoutMills)
---> nextPollTimeoutMills为-1,底层阻塞,因为刚才已经循环过了,发现mMessages已经没有消息了 - 如果不阻塞,就会去遍历,找到第一条能够执行的消息,如果马上执行就马上返回,如果不是,就设置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的产生
- 在启动Service的时候通过Handler发了个延迟消息(延迟时间根据前台还是后台设置) --> 埋下炸弹
- 如果在这个时间之前启动完成,会移除这个消息 --> 拆除炸弹
- 如果触发了这个消息,弹出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)
- LinkedHashMap:
参数:- initialCapacity:
- loadFactor:
- accessOrder:为true,访问LinkedHashMap里的元素后,该元素会放到这个链表的最后
- 统计监控:putCount、createCount、evictionCount、hitCount、missCount ---> 高端局的意识,20个字节的开销可以做到对LRU运行状态的了解
- sizeOf:默认是权重,默认是1,如果存的是Bitmap,图片大小就作为权重了
- get:小锁,没有加载方法上;加了两个短锁,只加在内部对象访问上,比如创建之类不涉及到LRUCache内部对象的访问(和put配合,Glide线程安全设计上,get是整个加了方法锁)
---> LRUCache设计出来,往往是需要多线程访问的,肯定是线程安全的 - trimSize:找到要移除的元素,android.util.LruCache里是最后一个元素,其实逻辑是错的,supportV4里是对的
<看了下api27是对的,通过eldest()拿到头节点>
- Glide版本
- 也LinkedHashMap,最近访问过的放到链表的最后
- put:相较于Android版本的get,没有create方法可供使用者自己实现缓存策略,避免自己实现的方法太长。简单粗暴的如果添加的元素超过最大限制,不添加
---> Glide的get就是简单的调用get方法 - 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
- 如果是jpg,是没有Aplha通道的,默认是ARGB_8888格式,但实际上RGB_565即可(5+6+5=16,两个字节)
==> 宽 * 高 * 2 - RGB_565的png也是如此
- 如果是从XXXdpi里读,还和屏幕大小有关,图片的大小会发生改变:
===>图片 / 所对应的dpi的值 * 屏幕的密度
(drawable为1,nodpi告诉系统不进行缩放,该多大多大)
(step1:图片放哪了如指掌,且能计算内存大小,那就可以选择合适的路径存放了)
Drawable中的图片加载流程
- BitmapFactory.decodeResource(Resouse,Int,Options)
- decodeResourceStream()<此处可以看到drawable和nodpi的处理>
- 最终调到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
- 通过注释@hide,写代码时android.jar里没有这个
比如convertFromTranslucent() --> 右滑返回讲过
- 自己打个带这个方法的jar包,系统里有的,运行时肯定没问题,除非系统把这个方法去掉了
- 所以反射肯定也是能拿到的
- 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为例
- 通过context.getDrawable,调用到Resource的getDrawable
- 根据是xml还是非xml,让AssetManager选择加载的方式
而context.obtainStyledAttributes调用Theme的obtainStyledAttributes,兜兜转转调用到AssetManager到applyStype
AssetManager.openAsset是开发者手动调的
(step1:对资源加载很熟)
资源缓存替换流
因为Resources里有些固定的字段
(sPreloadedDrawables/sPreloadedColorDrawables/sPreloadedComplexColors)
虽然随着版本不同,名字可能不一样,但总的来说还是有这些东西的,是加载资源进来的缓存
- 原先流程应该是走AssetManager拿资源的
- 但是我们可以预先把它从从Skin Resources里加载进来
- 所以就偷梁换柱了,Skin Resources里没有的时候,才会去AssetManager里找
Resources包装流
- 原先是通过调用Resources找资源的,但我们可以在这之前加一层ResourcesWrapper
- 让getDrawable/getColor/getText都先走ResourcesWrapper去找Skin Resources
- 所以如果Skin Resources里有资源,那就加载皮肤资源的,没有的按正常流程加载
AssetManager替换流
从根源上解决,所有经过AssetManager都可以改了,这个方案稍微厉害点。
- Native AssetManager里有mAssetPaths,可以有很多皮肤资源包,也包括系统的资源包
- 所以可以通过反射添加自己的资源包
方案对比 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一般来说是不会相同的,常量整型替换引用
动态映射方案
- 先拿到id的值
- 通过id可以找到名字,比如id/button
- 把Package从主包换成皮肤包的,通过名字映射回来,找到对应的正确的button的值
静态编译方案
- AAPT编译资源时输入主包的id映射,public.xml
- 编译后根据主包映射关系修改皮肤包的resources.arsc
资源增量静态对齐
对于attr,加载有些特点
- 假设主包里有3个Entry,皮肤包里有2个Entry
- 因为编译的时候会去检查Entry的个数,attr是按顺序排下来的 --> 也是为了让获取更有效
- 这时比如想要读取attr2,比皮肤包的entry个数大,那说明皮肤包里没有
- 也就不会继续走下去了,但我们希望如果皮肤包中不存在,读取主包的资源
- 所以需要给皮肤包没有的资源用空值强制占位,cheat一下,这样在皮肤包里找不到就会去主包找了
另外:
- R.attr.attr1皮肤包中为定义,编译时AAPT会报错
- 若剔除public.xml的R.attr.attr1,编译时后续非public的资源会顺序占坑
---> 也是为了保持资源的紧凑
这样比如找attr1,对应皮肤包的就不对了,整个就乱掉了
解决:定制apt或者修改资源包,让它支持没有资源的占坑,皮肤包里不存在就可以找到主包里的了
定制AAPT实现占坑
ResourceTable::applyPublicEntryOrder
上述的 找不到占坑 + 最后没有了占坑
(更改AAPT和后期维护不容易)
---> 运行时增量替换是相对麻烦的
简单方案:皮肤包资源增量差分方案
- 通过主包和皮肤包的差异,差出来一个差分包
- 客户端拿到主包和差分包,合成一个完整的皮肤包,运行时直接替换掉
---> 替换新的AssetManager时,只需要添加一个AssetPath
问题:
- 重定向问题还是会有
- 因为皮肤包有所有的资源,加载到内存里会大一些
- 合成的时候也有一定开销,如果皮肤包比较大,会比较耗时
AssetManager替换流的实现
- 反射拿到addAssetPath方法
- 加载方式版本不同有些差异:
- 4.0先添加主包的资源,再添加皮肤包
- 5.0拿到皮肤包后需要先进行拆分,把asset里的文件单独拿出来,先加载皮肤包
再加载主包、最后加载Asset
---> 原因是加载顺序:AssetFile读的时候,优先读最后加进来的资源包,跟资源不一样。4.0和5.0加载顺序是反的,但AssetFile和添加顺序正好是一致的
<我们目的是优先读皮肤包>
(不太理解)
- 替换AssetManager(有很多种),比如可以通过包装Context,因为ContextWrapper里持有AssetManager
---> 为什么要解包装?因为在ActivityThread里,在一个收广播(印象中)地方,会判断类型,如果不是ContextImpl就会抛异常了
- 如何让每个Activity覆写attachBaseContext和getBaseContext?
- 都继承自BaseActivity --> 但会改变继承结构(试试代理?)
- 通过Javasist修改字节码完成自动注入(例如RePlugin)
换肤和插件化是有差异的
- 换肤框架要保证资源id不变,是覆盖关系
- 插件化框架资源id不同,是并存关系
- 插件化框架宿主资源共享不存在覆盖
(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插件 |
如何加载运行插件代码
- LoadedPlugin持有Apk信息,类似系统的LoadedApk持有Apk信息
- 加载插件代码方式有两种:
- 如果是Combine_ClassLoader,需要加载到宿主里面(不需要隔离)
- 如果不是,就不用加载到宿主里,实现隔离
---> 需要注意的是,创建的DexClassLoader的parent是宿主的ClassLoader<PathClassLoader>,所以实际上隔离不隔离都可以用反射拿到宿主里的类的(那为何要特地插入呢?)
对应映射图(TODO:06:55)
- 插入宿主的原理:把dex插到后面,插入完以后就宿主里的ClassLoader就成了巨无霸了,既能加载宿主里的类,又能加载插件里的类
---> 与QQ空间超级补丁的热更新不同,超级补丁插到最前面,优先加载 ,毕竟不是为了修复
对比:DroidPlugin的超强隔离
把插件的DexClassLoader和宿主的PathClassLoader,位与双亲委派的同级,且插件之间是又是不可见的,所以完全隔离
(step1:万里长城第一步,解决了类)
如何处理插件资源
和加载类一样,分为两种模式 ---> 和滴滴的业务很有关系
- 如果是Combine_Resource,加载宿主和插件的资源(不需要隔离)
- 如果不是,只加载插件的资源
资源编译处理及过滤 --> 没有combine,只有Plugin,加载起来什复杂的地方
和换肤还是有差别的,换肤需要id来映射,但插件化不需要,可以通过编译的时候过滤掉重复的id
--> 在编译的时候可以更改(把没有重复的资源,用他的包的标识来确保不重复)
- 注意R文件也要改,相当于重定向了
- apt编译,资源表是顺沿的,保持连续
插件和宿主资源没有重复(编译过滤)
插件资源id的package被修改
---> 引申
资源过滤存在的问题
Q1:插件开发和宿主开发都是独立的,如果有一方修改了对比用的资源表(id不同了),虽然插件编译可以成功,但是运行的时候通过这个id,肯定是找不到了的(因为都乱套了)
---> 为什么是插件里找不到?想想ClassLoader是先加载宿主的,所以插件的R.java不会被加载
解决:
- 把插件的R文件的final去掉,编译期间就不会用插件的R来替换了,所以用的一直是宿主的R文件
---> 需要了解类加载和编译期常量才能想到
缺陷:没办法解决xml里的引用,因为编译完以后,由apt直接替换了
---> 加载资源的时候去Hook(比较麻烦了);或者宿主不变,利用public.xml,但是资源是按顺序排的,所以这个文件只能增加,不能删;
(这个方案可以作为只有Java代码的方式的候补方案)
Q2:比如app_name/about类似的资源,宿主和插件定义的名字可能重复
(资源名称相同,资源本身不同)
(step2:能把资源加载的细节给讲清楚,面试体验会非常好,给面试官感觉像是学术交流)
如何支持启动插件Activity
熟悉Activity启动流程中知道,AMS不知道插件里面有哪些组件,因为需要解析Manifest注册的,插件里并没有注册。
解决:通过占坑的Activity,欺上瞒下
- 启动过程中,拦截到这个启动插件Activity的请求(第一个Hook点) --> 替换Intent
- 告诉AMS启动占坑的Activity
- 回到自己的进程,通过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:
- 动态代理替换AMP(AMS在客户端的代理)
- 发送前包装Intent,即把启动Service的Intent作为extra
- LocalService代理目标服务,通过反射拿到需要启动的Service,指向对应操作
<启动Service的时候就包装了,没有Activity的反序列化问题了>
---> 和DroidPlugin很像 (看下当前版本的VirtualAPK)
如何支持注册广播
- 解析插件Manifest,静态广播转动态注册
- 插件广播在宿主未运行时无法被外部唤醒
---> 因为变成动态广播了,AMS不知道,一旦宿主挂了,就没办法被拉起来了,所以需要保活的需要放在宿主里 - 系统限制只能静态注册的广播可在宿主预埋并处理
---> 比如需要开机自启的
如何支持注册插件ContentProvider
---> 实现思路类似,自行阅读源码
本节回顾:
- 分析VA如何支持插件类加载
- 对比DroidPlugin的类完全隔离方案
- 分析VA如何支持插件资源加载
- 探讨VA资源过滤处理的使用场景以及问题
- 分析VA如何支持插件Activity的启动
- 探讨Intent的Extras反序列化的问题
- 分析VA如何支持插件Service的启动
4. Tinker如何实现热修复?
考察点:
- 是否有过热修复的实战经验?(中级)
- 是否清楚热修复方案如何对代码进行更新?(高级)
- 是否清楚热修复方案如何对资源进行更新?(高级)
- 是否具备框架设计开发的技术功底和技术素养?(高级)
题目剖析:
关键词:热修复
- 不一定讲Tinker,说你熟悉的
- 如何支持代码的热修复
- 如何支持资源的热修复
Tinker工作流程
- 修复后的APK和基准APK,差异出patch.zip
- 把patch.zip下发到用户,组合成修复的Apk
- 工作启动的还是基准包,把修复完成的Apk的Dex放到基准包的Dex前面,那ClassLoader就可以优先加载了。资源的修复比较简单,直接替换掉就行了。 --> 所以后面下发的,前面的就失效了
(step1:知道基本工作流程,是了解原理的基础)
对比 QQ空间超级补丁:如果只包含修复的类,如果这个类被其他的类引用,这个类会报is_pre_verified的异常
---> 因为Tinker是整个的dex,所以会很大
Java代码修复 - 基于Dex的差分算法
DexSectionDiffAlgorithm ---> 难点在于这个数据结果
DexSectionDiffAlgorithm.execute():
先排序,再比较,通过两个分别指向old和new指针
- 只剩新包的元素,一定是Add的
- 只剩基准包的元素,一定是Delete的
- 中间的需要比较,old<new一定是删除的,old>new一定是新增的,old==new,没变,但需要记录位置和offset(offset也是为了优化)
- 连续相同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很极致
细致的异常处理
- 校验MD5
- 统计耗时
- 失败回滚 --> 卸载热修复的包
异常熔断
监控&闭环意识
Tinker的监控代码埋的很多,平均一两百行就有一处监控,早期版本就说有129处
良好的注释
Tinker的注释量也很多,占比20%
(step:代码规范和把控很大)
本节回顾:
- 探讨Tinker的工作机制
- 分析Tinker DexDiff算法的思路
- 探讨Tinker的Dex加载机制
- 探讨Tinker的资源热修复机制
- 探讨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单/天)
人力优化
- 主要针对项目负责人
- 需要堆项目成员的能力有足够的了解
- 需要对项目功能做合理的拆解
- 用合适的人做合适的事
- 适当放权,但也要依据情况做好辅导
优化心法
- 深入钻研技术为优化提供可能性
- 结合业务场景为优化提供落脚点
- 熟悉团队特点为优化提供战斗力
比如算法优化:
- 如果涉及大量的矩阵运算,Java层运算会导致频繁GC,可以考虑小矩阵池化,减少对象的频繁创建和频繁GC
- 如果JNI调用频次很高,可以考虑C重写,而且直接使用物理内存,减少GC
比如业务优化:(比如入库耗时)
- 加密结果较大是源数据较大导致,在探讨后,不影响业务的前提下优化了数据格式,将JSON格式的源数据改为Protobuf进行加密,源数据减少了60% ---> 项目中尝试下
- 源数据存储与sqlite,实验发现其二进制读写性能不如文件直接读写,因此同样不影响数据的情况下直接从文件系统读取,性能提升约5%
- 将算法做了优化,确保安全性的前提下,由原先的全文件加密,改为局部加密,文件不需要完整加载和回写,直接随机读写文件系统就可以解决,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. 如何解答系统设计类问题?
---> 对于基础比较好的,非常友好的问题
考察点:
- 是否能够快速理解需求并对需求进行拆解(中级)
- 是否具备广泛的技术栈或知识面(高级)
- 是否能够深入挖掘需求给出良好的技术方案(高级)
- 是否具备良好的项目管理和领导能力(高级)
题目剖析:
关键词:系统设计类
- 解答过程中与面试官要保持良好的沟通
- 如果系统足够大,则不需要解释太多细节
- 如果系统较小,最好辅以精妙的细节设计
项目诞生记:
提出想法 --> 可行性研究 --> 需求分析 --> 系统设计 --> 系统开发 --> 迭代维护 --> 系统重审
面试官:提出想法
候选人:需求分析、系统设计
系统设计步骤:
- 需求:设计(项目需求)一个网络请求框架
- 关键流程:关键就是打包请求、建立连接、发送请求、解析结果
- 细节:请求和响应数据结构适配能力(Adapter)、请求重试机制(拒绝策略)、异步处理能力、使用体验优化
回顾:如何用Java实现Handler
- 需求:移植Android Handler 到 Java平台
- 流程:关键消息队列、死循环、阻塞和延时
- 细节:是否需要支持底层、消息队列性能优化、消息实例池化
系统设计三步走:
明确边界 --> 打通流程 --> 优化细节
常见细节:
- 如何处理好并发?
- 是否有频繁的IO操作?
- 线程调度如何设计?(线程池使用限制、线程数?)
- 业务操作中异步程序如何设计?
- RxJava
- 协程(Kotlin)
- 网络如何接入?
- 是否需要频繁与服务端交互?(根据需求是否短连接即可,很频繁考虑多拉些数据、连接池化)
- 是否存在服务端主动推送消息的场景?
- 采取何种通信手段?
- 长连接:高频交互,消息推送,维护复杂
- 短连接:低频交互,伪消息推送(短轮询、长轮询)
短轮询:每隔一段时间请求服务端;
长轮询:发一个请求,服务端不返回,直到有消息再返回,一直没消息,比如经过60秒,客户端可以重新发起请求。
相比之下:短轮询有延时问题,长轮询延时比较小,维护成本相对于长连接开销比较小
-
保障安全性
- 数据是否需要加密?(避免被竞品爬走)
- 加密算法如何选择?(需要业务和体验的结合,视频每个字节加密,解密就很卡了)
- 对称加密:密钥如何保存?
- 非对称加密:注意加密复杂度限制(耗时长)
---> 通常用对称加密对数据进行加密,用非对称加密对对称加密对密钥进行加密(https好像就是)
- 应用安全性如何保证?
---> 危害:被破解,植入广告(混淆、加固、验签)
-
热修复与插件化
热修复一般都需要,关键看方案选型
- 是否要求立即生效? --> 有些app是开机一直运行的
- 是否要求新增或修改类?<这个有不需要多情况吗>
插件化主要考虑体量
- 前期通常不需要插件化,但可未雨绸缪
- 是否融合了多条业务线,多团队协作?
- 脚本化
<二八定律:80%的版本都是20%的代码需要修改>
- 是否存在大量可模式化的逻辑?
- 游戏关卡
- 自定义的UI体系
- 是否存在大量需要经常调整的策略? --> 经常调整
- 简单的参数调整无法满足
- 可移植性:
- 是否存在平台不相关逻辑?
- 如语音识别、OpenGL绘制逻辑
- 考虑C++开发(可以在win上跑,便于调试)
- 是否考虑移植UI?
- Flutter/React Native
(常用套路:上层UI用flutter,下层业务逻辑选择平台无关的语言,比如kotlin native)
- Flutter/React Native
- 性能问题:
- 算法复杂度和时间复杂度?
- 内存峰值是否偏大有无OOM可能?(图片多吗?)
- CPU占用率是否持续较高?(视频播放->软解码,硬解码是否支持该格式?)
- 耗电量是否高居不下(是否一直需要屏幕亮着)
TODO:常见操作的耗时 --> 可以心里有数,规避一些瓶颈的要点
- 监控:
--> 高级工程师意识的体现,要注意反馈,形成一个闭环
- 异常捕获以及状态保存恢复
- 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 |
- 针对热点视频采用H.265 --> 很少用了H.265,稍微慢一点用户无感知,但是成本减少了
- 针对性能较好的机型动态切换软解H.265与硬解H.264
播放优化
- 根据MP4文件的格式内容:fytp,moov(索引之类)、mdat(数据)
[ftyp-mdat-moov]会导致无法边下边播,索引上传了这样格式的,在服务器进行转码,首先收到moov,通常不大 - 播放器行为限制: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都有
- 修改请求:全局参数,登陆状态信息
- 处理共用的结果返回
- 模拟服务能力
- 日志工具:打印结果,方便调试
重试机制
<拒绝策略>
- 可设置最大的重试次数
- 可指定频率衰减因子
(step2:对网络请求流程和需要的东西有所了解)
使用注解配置请求
协议是很模版化的东西,写着很累 --> 很多框架都采用注解简化配置,例如Spring、Retrofit
第三方扩展
比如支持Kotlin和RxJava(比如Retrofit)
代码设计模式
- 协议体构建使用Builder模式
- 数据的传输与拦截使用责任链模式
- 数据序列化类型支持使用适配器模式(+泛型应用)
主要设计的高级语法
- 注解:主要用于接口配置和参数解析
- 泛型:主要用于数据类型的适配
- 反射:读取注解信息、反序列化类型等等(配合前二者)
DNS增强
<默认是通常是去运营商查询,有可能返回的是一个被劫持的,比如植入广告的ip> --> 第三方DNS就这么容易被劫持吗?
让网络框架支持DNS查询,如果自己公司有的话,请求自己公司的,一般是拿到一串IP,一个不成功请求下一个,如果没有可以找一些大的厂商,一般都会有的(比如阿里、腾讯)。
(step3:思考的蛮多的,没遇到过劫持一般想不到)
本节回顾:
- 设计一个网络请求框架
- 分析需求,确定系统功能边界
- 打通流程,抓住关键网络请求流程
- 优化细节,拦截器、第三方支持等等
(三大步骤,十个方面想一想,很有可能思如泉涌)
第12章 课程总结
题目本身不重要,关键是回答的思路,触类旁通
要不断和面试官沟通,面试官要看候选人的表达能力,沟通能力
网友评论