Android开发

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

2020-01-16  本文已影响0人  New_X

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

1. Android中为什么非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吗?

本节回顾:

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

考察点:

题目剖析:
关键词:可靠

主线程正常工作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可靠吗?

本节回顾:

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

考察点:

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

ANR的类型

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:基础非常扎实)

本节回顾:

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

考察点:

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

Handler的核心能力

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

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

MessageQueue的核心能力

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

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

测试代码
很像ActivityThread

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

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

回顾Android的Looper对epoll的运用

Android为什么不直接复用DelayQueue

本节回顾:

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

1. 如何避免OOM到产生?

考察点

题目剖析:
关键词:OOM

OOM的产生

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

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

避免使用枚举 TODO:06:43

避免使用枚举,会有类型安全问题,所以引入了注解 --> 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的使用

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

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

使用NDK

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

推荐Android性能优化典范

本节回顾:

技巧点拨:

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

考察点:

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

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

缓存算法
考虑点:

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

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

LRU算法的实现

  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()拿到头节点>
  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中的图片
图片越大,文件肯定越大,但是数值上没有直接的关系
计算占用内存:
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>

图片内存体积优化

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

本节回顾:

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

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

考察点:

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

私有API

  1. 通过注释@hide,写代码时android.jar里没有这个
    比如convertFromTranslucent() --> 右滑返回讲过
  1. private修饰

访问私有API

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. 如何实现换肤功能?

题目剖析:
关键词:换肤

系统的换肤支持-Theme

资源加载流程 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的一致性处理
资源重定向 无此问题 运行时动态映射;编译期静态对齐(可选) 编译期静态对齐

----> 追究细节
资源重定向:默认编译生成的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. 加载方式版本不同有些差异:
  1. 替换AssetManager(有很多种),比如可以通过包装Context,因为ContextWrapper里持有AssetManager
    ---> 为什么要解包装?因为在ActivityThread里,在一个收广播(印象中)地方,会判断类型,如果不是ContextImpl就会抛异常了
  1. 都继承自BaseActivity --> 但会改变继承结构(试试代理?)
  2. 通过Javasist修改字节码完成自动注入(例如RePlugin)

换肤和插件化是有差异的

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

本节回顾:

3. 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)

如何支持注册广播

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

本节回顾:

4. 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. 如何开展优化类工作?

考察点:

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

明确优化的目标:

定性到定量的改变:

定位关键问题:

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

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

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

项目收益:

人力优化

优化心法

比如算法优化:

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

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

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

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

本节回顾:

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

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

优化前的项目状况

量化指标

对比现有技术方案

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

监控体系建设

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

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

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

直接指标量化

本节回顾:

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

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

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

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

本节回顾:

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

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

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

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

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

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

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

系统设计步骤:

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

回顾:如何用Java实现Handler

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

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

常见细节:

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

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

  1. 保障安全性

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

插件化主要考虑体量

  1. 脚本化
    <二八定律:80%的版本都是20%的代码需要修改>
  1. 可移植性:
  1. 性能问题:
  1. 监控:
    --> 高级工程师意识的体现,要注意反馈,形成一个闭环

思考过程:

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

2. 设计一个短视频App

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

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

明确需求边界

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

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

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

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

安全性

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

指标 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自研播放器,收到关键帧就可以播放了,既可以实现抖音一样的秒播

流量合作

本节回顾

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

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

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

明确需求边界

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

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

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

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

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

重试机制
<拒绝策略>

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

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

代码设计模式

主要设计的高级语法

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

本节回顾:

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

第12章 课程总结

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

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

上一篇 下一篇

猜你喜欢

热点阅读