recyclerview
- Recycler有4个层次用于缓存ViewHolder对象,优先级从高到底依次为
ArrayList mAttachedScrap
、ArrayList mCachedViews
、ViewCacheExtension mViewCacheExtension
、RecycledViewPool mRecyclerPool
。如果四层缓存都未命中,则重新创建并绑定ViewHolder对象
- 缓存性能:
缓存 | 重新创建ViewHolder |
重新绑定数据 |
mAttachedScrap | false | false |
mCachedViews | false | false |
mRecyclerPool | false | true |
- 缓存容量:
- mAttachedScrap:没有大小限制,但最多包含屏幕可见表项。
- mCachedViews:默认大小限制为2,放不下时,按照先进先出原则将最先进入的
ViewHolder
存入回收池以腾出空间。
- mRecyclerPool:对
ViewHolder
按viewType
分类存储(通过SparseArray
),同类ViewHolder
存储在默认大小为5的ArrayList
中。
- 缓存用途:
- mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用。
- mCachedViews:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到
mCachedViews
,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder
存入回收池。
- mRecyclerPool:用于移出屏幕表项的回收和复用,且只能用于指定
viewType
的表项
- 缓存结构:
- mAttachedScrap:ArrayList
- mCachedViews:ArrayList
- mRecyclerPool:对ViewHolder按viewType分类存储在SparseArray中,同类ViewHolder存储在ScrapData中的ArrayList中
ItemDecoration
- 用于绘制ItemView以外的内容,有两个回调onDraw和onDrawOver,区别是绘制的顺序有先后,onDraw()会在RecyclerView.onDraw()中调用,表示绘制RecyclerView自身的内容,会在绘制孩子之前,所以出现在孩子下面,而onDrawOver() 是在 RecyclerView.draw()中调用,在绘制孩子之后调用,所以会出现在孩子上方
view生命周期
构造View --> onFinishInflate --> onAttachedToWindow --> onMeasure --> onSizeChanged --> onLayout --> onDraw --> onDetachedFromWindow
Bitmap
- raw和drawable和sdcard,这些不带dpi的文件夹中的图片被解析时不会进行缩放(inDensity默认为160)
- 获取bitmap宽高:若inJustDecodeBounds为true,则不会把bitmap图片的像素加载到内存(实际是在Native层解码了图片,但是没有生成Java层的Bitmap),只是获取该bitmap的原始宽(outWidth)和高(outHeight)
- 在4.4之前如果复用bitmap则不支持在native层缩放,缩放放到java层,将原来bimap基础上新建bitmap,销毁原来的,这样效率低, 4.4以后支持在native层做缩放
- 8.0,bitmap像素数据在native,6.0以前finalize释放native的bitmap,之后通过注册NativeAllocationRegistry(简化了Cleaner的使用),将native资源的大小计入GC触发的策略之中。即使java堆增长缓慢,而native堆增长快速也同样会触发gc
- Cleaner用于回收native堆中的像素数据:Cleaner继承自虚引用,虚引用指向对象被回收时,虚引用对象会进入ReferenceQueue,异步线程ReferenceQueueDaemon会在ReferenceQueue.wait()上等待,只有对象被回收,然后遍历引用队列,若存在Cleaner则调用clean方法释放native内存
- Bitmap大小=长* 宽 * 像素大小
- BitmapFactory.Options
- 复用bitmap:加载时设置inBitmap表示使用之前bitmap对象使用过的内存 而不是重新开辟新内存(如果被复用的Bitmap == 返回的被加载的Bitmap,那么说明复用成功了)。复用条件是,图像是可变的isMutable为true
- inPreferredConfig
- 只有当图片是webp 或者png24的时候,inPreferredConfig才会有效果。
- ALPHA_8 : 图片只有alpha值,没有RGB值,一个像素占用一个字节
- RGB_565:2字节
- ARGB_8888 : 一个像素占用4个字节
- inSampleSize:为2的幂次,表示压缩宽高为原来的1/2,像素密度不变,按需加载,按ImageView大小加载,计算inSampleSize的算法是,原始宽高不停除2,inSampleSize不停乘2,直到原始宽高小于需求宽高。
- 回收bitmap(Bitmap.recycle()):只是释放图片native对象的内存,并且去除图片像素数据的引用,让图片像素数据可以被垃圾回收
- Bitmap占用内存大小因素:
- 图片的原始宽高(即我们在图片编辑软件中看到的宽高)
- 解码图片时的Config配置(即每个像素占用几个字节)
- 解码图片时的缩放因子(即inTargetDensity/inDensity)
- BitmapRegionDecoder用于图片分块加载
- jpg 色彩丰富,没有兼容性问题,不支持透明度,和动画,适用于摄影作品。jpg在高对比度的场景下效果不好,比如黑色文字在白色的背景上
- png包括透明度适用于图标,因为大面积的重复颜色,适用于无损压缩
- webp包括有损和无损两个方式,webp的浏览器支持不佳,webp支持动画和透明度,之前动画只能用gif,透明度只能选png,有损压缩后的webp的解码速度慢,比gif慢2倍
- 图片缩放比例 scale = 设备分辨率 / 资源目录分辨率 如:1080x1920的图片显示xhdpi中的图片,scale = 480 / 320 = 1.5,图片的宽高会乘以scale
ANR
- KeyDispatchTimeout:View的点击事件或者触摸事件在特定的时间(5s)内无法得到响应。
- BroadcastTimeout:广播onReceive()函数运行在主线程中,在特定的时间(10s)内无法完成处理。
- ServiceTimeout:Service的各个生命周期函数在特定时间(20s)内无法完成处理
Lifecycle
- 让任何组件可以作为观察者观察界面生命周期
- 通过LifecycleRegistry,它持有所有观察者,通过注册ActivityLifecycleCallbacks 实现生命周期的分发,如果是29 以下则将ReportFragment添加到activity中
- 监听应用前后台切换:通过registerActivityLifecycleCallbacks,然后在维护一个活跃activity的数量,ProcessLifecycleOwner为我们做了这件事情 用于监听应用前后台切换,ProcessLifecycleOwner的初始化通过ContentProvider实现
恢复数据
- onSaveInstanceState()+onRestoreInstanceState():会进行序列化到磁盘,耗时,杀进程依然存在
- Fragment+setRetainInstance():数据保存在内存,配置发生变化时数据依然存在,但杀进程后数据不存在
- onRetainNonConfigurationInstance() + getLastNonConfigurationInstance():数据保存在内存,配置发生变化时数据依然存在,但杀进程后数据不存在
进程优先级
一共有五个进程优先级
- 前台进程(Foreground process):该进程中有前台组件正在运行,oom_adj:FOREGROUND_APP_ADJ=0
- 正在交互的Activity,Activity.onResume()
- 前台服务
- Service.onCreate() onStart()正在执行
- Receiver.onReceive()正在执行
- 可见进程(Visible process) VISIBLE_APP_ADJ = 1
- 正在交互的Activity,Activity.onPause()
- 服务进程(Service process)
- 后台服务
- 后台进程(Background process) BACKUP_APP_ADJ = 3
- 不可见Activity Activity.onStop()
- 后台进程优先级等于后台服务,所以长时间后台任务最后其服务
- 空进程(Empty process):不包含任何组件的进程,Activity 在退出的时候进程不会销毁, 会保留一个空进程方便以后启动. 但在内存不足时进程会被销毁
按下返回键退出应用,此时应用进程变成缓存进程,随时可能被杀掉
按下home键退出应用,此时应用是不可见进程
LruCache
- ~是内存缓存,持有一个 LinkedHashMap 实例
- ~用LinkedHashMap作为存储结构,且LinkedHashMap按访问顺序排序,最新的结点在尾部,最老的结点在头部
启动优化
- 视觉优化:windowBackground设置一张图片(成为StartingWindow的Decorview的背景)
- 初始化任务优化:可以异步初始化的,放异步线程初始化,必须在主线程但可以延迟初始化的,放在IdleHandler中,
- ContentProvider 优化:去掉没有必要的contentProvider
- 缩小main dex:MultidexTransform 解析所有manifest中声明的组件生成manifest_keep.txt,再查找manifest_keep.txt中所有类的直接引用类,将其保存在maindexlist.txt中,最后将maindexlist.txt中的所有class编译进main.dex。multiDex优化,自行解析AndroidManifest,自定义main.dex生成逻辑,将和启动页相关的代码分在主dex中,减小主dex大小,加快加载速度。
- multiDex.install 异步化,在4.4以下的机型MultiDex.install()耗时。它会先解压apk,遍历其中的dex文件,然后压缩成对应的zip文件(这是第一次的逻辑,第二次启动时已经有zip文件则直接读取。)然后通过反射,将其余的dex追加到DexPathList的尾部。这个过程中的压缩成zip可以免去,以提升速度。可以将这个过程放在单独的一个进程中做(在attachBaseContext中,开启一个死循环等待multidex完成),而且该进程有一个activity界面展示loading,但加载完毕后通知主进程,在跳转到闪屏页
LiveData
- LiveData 的数据观察者在内部被包装成另一个对象(实现了 LifecycleEventObserver 接口),它同时具备了数据观察能力和生命周期观察能力
- LiveData 内部会将数据观察者进行封装,使其具备生命周期感知能力。当生命周期状态为 DESTROYED 时,自动移除观察者
- LiveData 的观察者会维护一个“值的版本号”,用于判断上次分发的值是否是最新值,“新观察者”被“老值”通知的现象叫“粘性”。因为新观察者的版本号总是小于最新版号,且添加观察者时会触发一次老值的分发
- 粘性不应该成为一个问题,网上有很多关于粘性的解决方案,详见LiveData 面试题库、解答、源码分析
- 在高频数据更新的场景下使用 LiveData.postValue() 时,会造成数据丢失。因为“设值”和“分发值”是分开执行的,之间存在延迟。值先被缓存在变量中,再向主线程抛一个分发值的任务。若在这延迟之间再一次调用 postValue(),则变量中缓存的值被更新,之前的值在没有被分发之前就被擦除了。
ViewModel
- ViewModel 实例被存储在ViewModelStore的map中, 在配置发生变化时onRetainNonConfigurationInstance会被调用(ViewModelStore的map对象会被存储在NonConfigurationInstances中),并会返回这个对象.在恢复ViewModel时再getLastNonConfigurationInstance中再次获取
- Activity 实现了LifecycleOwner,等onDestroy时会尝试(非配置变化时)调用 store的 clear(遍历了 viewmodel的clear)
- ViewModel 在 Fragment 中不会因配置改变而销毁的原因其实是因为其声明的 ViewModel 是存储在 FragmentManagerViewModel 中的,而 FragmentManagerViewModel 是存储在宿主 Activity 中的 ViewModelStore 中,又因 Activity 中 ViewModelStore不会因配置改变而销毁,故 Fragment 中 ViewModel 也不会因配置改变而销毁。
DiskLruCache
- 内部有一个线程池(只有一个线程)用于清理缓存
- 内部有一个LinkedHashMap结构代表内存中的缓存,键是key,值是Entry实体(key+file文件)
- ~有一个journal文件缓存操作的日志文件,构造时会读取日志文件并将其转换成LinkedHashMap存储在内存
- 取缓存时,先读取内存中的Entry,然后将其转换成Snapshot对象,可以从Snapshot中拿到输入流
- 写缓存时,新建Entry实体并存在LinkedHashMap中,将其转换成Editor对象,可以从中拿到输出流
- 通过LinkedHashMap实现LRU替换
- 每一个Cache项有四个文件,两个状态(DIRTY,CLEAN),每个状态对应两个文件:一个文件存储Cache meta数据,一个文件存储Cache内容数据
编译打包流程
- 打包资源,res下文件转换成二进制,asset目录
- 编译java文件为class文件
- 将class文件转换为dex文件
- 将资源和dex打包到apk
- 签名
launch mode
taskAffinity与allowTaskReparenting配合:我们可以在AndroidManifest.xml为Activity配置android:allowTaskReparenting属性,表示允许此Activity更换其从属的任务栈。设置此属性的Activity一但当前Task切换到了后台,就会回到它“倾向”的任务栈中
- singleTask 全局唯一,先检查是否有和待启动Activity的taskAffinity相同的task(若未显示指定,则默认和app的第一个activity拥有相同的taskAffinity,为包名),若无则新建task并新建Activity压栈,若有则把该task移到前台并在该task中寻找待启动activity,若找到则将该task中该activity之上的所有activity弹出,让其成为栈顶(此时onCreate()不被调,onNewIntent()被调),若没有找到则新建Activity实例并压栈
- singleInstance 全局唯一,如果不存在待启动Activity,则新建task来容纳待启动activity,并且该task不能放入其他activity,若存在,则将对应的task移到前台 该类型只能在manifest中指定,不能通过Intent.setFlag() 该类型只能作用于Task栈底的Activity
- standard 全局不唯一,每次都在当前task中新建一个实例(待启动activity.onCreate()每次都会被调用)
- singleTop 全局不唯一,只有当Activity位于task的栈顶时,该activity实例才会被重复利用(onNewIntent()被调用而不是onCreate()),否则都会新建实例
ActivityManagerService
- 在SystemServer中被启动,通过SystemServiceManager.startService(ActivityManagerService.Lifecycle.class)启动
- 负责四大组件的启动切换调度
- AMS 构造的时候
- 创建了两个线程,一个是工作线程,一个是和进程启动相关的线程
- 启动了低内存检测LowMemDetector,通过epoll机制获取低内存通知
- 构建ActiveServices管理service
- 构建ProviderMap管理contentProvider
- 初始化ActivityTaskManager
- 开启Watchdog,监听线程阻塞
- 创建OomAdjuster用于调整进程的优先级等级
- 创建BatteryStatsService和ProcessStatsService用于管理电量状态和进程状态
- 获取AMS对象,是通过 ServiceManager.getService() 获取一个 IBinder 对象,然后通过asInterface()获取本地对象或者远程对象的本地代理(Android 10中所有系统服务都通过AIDL接口来获取,在Android10以前获取服务不是通过直接通过AIDL接口的,而是通过ActivityManagerNative来转发,本质还是通过AIDL生成类Stub来获取)
- 它负责管理Activity,它通过ActivityStackSupervisor管理Activity调度,ActivityStackSupervisor实例在它构造函数中被创建
- 它会和应用进程双向通信以完成启动Activity,
- 它维护所有进程信息,包括系统进程,它通过ProcessRecord来维护进程运行时的状态信息,需要将应用进程绑定到ProcessRecord才能开始一个Application的构建
动画
- view animation
- 也称为补间动画:定义关键帧,其余帧由系统补齐,有点像导航
- 局限于view 局限于位移 旋转 透明度 缩放
- 用父控件的Transformation中的matrix,将matrix应用到View 上,每次绘制的时候会检查动画是否完成,若没完成则调用invalidate(),ViewRootImpl向编舞者跑了一个遍历View树的任务,会有同步消息屏障
- 不能监听动画变化的过程
- 还能响应原有位置的触摸事件,是因为会将 matrix 反向运算
- property animation
- 构建值变化的完整序列 并将其运用到视图属性上
- 不仅仅适用于view 可用于任何提供了getter和setter的对象的任何属性上
- 通过向 Choreographer 不停post 一个动画类型任务(在绘制任务之前执行), 当前帧完毕后若动画未结束继续post 没有同步消息屏障
- 可监听动画变化过程
- Interpolator决定值变化的速度(根据时间流逝的百分比计算出当前属性值改变的百分比)
- 若是硬件加速,则直接修改RenderNode中的相关属性,不需要重新构建DisplayList,若是软件绘制,则会触发invalidate
- 帧动画:就像连环画一样,一张张图片连续播放。AnimationDrawable 会在动画播放之前将所有帧都加在到内存,耗内存。
关于帧动画的性能优化可以点击Android性能优化 | 帧动画OOM?优化帧动画之SurfaceView逐帧解析
ConstraintLayout 性能
- 额外的封装,会将容器控件包装成ConstraintWidgetContainer,子控件包装成ConstraintWidget
- 在测量布局的时候,会想把所有的ConstraintWidget移除,然后遍历所有子控件并重新添加ConstraintWidget,如果子控件有约束则将其连接到锚点,构建依赖图,深度遍历依赖图,进行求解(Cassowary 算法),最终得到相对于父亲的上下左右。
Java & Kotlin
string
string是final类型的char数组,表示引用不会改变
final
表示引用指向不能变,但其指向的变量是可变的
泛型
- 泛型的目的是类型参数化,即用变量表示类型
- 提升安全性:泛型可以把使用Object的错误提前到编译后,而不是运行后,提升安全性
- 消除强转:没有泛型的时候,都用Object代替,因为Object可以强转成任何类型
- PECS是在使用泛型时为了遵守里氏替换原则必须准守的原则,使用泛型增加代码适用性时保证了类型安全。
- PE:Producer extends 实现协变效果:泛型类和类型参数的抽象程度具有相同的变化方向。泛型类只生产泛型,即泛型只会出现在类方法的返回值位置,kotlin中用out表示,java用extend表示
- CS:consumer super 实现逆变效果:泛型类只消费泛型,即泛型只出现在类方法的参数位,kotlin中用in表示,java用super表示。
- 类型参数的父子关系是否会延续到外部类上,若延续的叫协变,否则父子关系转向了,这叫逆变,若没有父子关系则叫不变型 ,泛型是不变型
- 类型擦除:为了兼容1.5以前的代码,即编译后的实参类型是Object或者上界
- 当子类覆盖或者实现父类方法时,方法的形参要比父类方法的更为宽松;
- 当子类覆盖或者实现父类方法时,方法的返回值要比父类的更严格。
- 如果在编译的时候就保存了泛型类型到字节码中,那么在运行时我们就可以通过反射获取到,如果在运行时传入实际的泛型类型,这个时候就会被擦除,反射获取不到当前传入的泛型实际类型
- Kotlin reified 可避免类型擦除,它会将方法内联到执行的地方,并且对泛型对象进行instanceof 的分类讨论。
关于泛型的详细分析可以点击Kotlin 进阶 | 不变型、协变、逆变
java对象生命周期
- 创建:为对象分配内存,构造对象
- 应用:至少一个强引用指向它
- 不可见:不再持有强引用(程序执行超出了对象的作用域)
- 不可达:没有强引用指向
- 收集:准备gc,会执行 finalize()
- 终结:等待垃圾回收
- Deallocated:回收完成
类加载
- 编译:javac 命令把 .java 文件编译成字节码(.class 文件)
- 运行:jvm执行.class
- 类加载过程:加载---链接---初始化
- 类加载:jvm把.class作为二进制流读入内存,并实例化一个Class对象,jvm 并不是一次性把所有类都加在到内存,而是执行过程中遇到没有加载的才加载,并只加载一次(Android加载的dex)
- 验证:二进制合法性校验
- 准备:为类变量在方法区赋初始值
- 解析:将类名,方法名,字段名替换为内存地址
- 初始化:对类的主动引用,包括new 调用静态方法,使用静态字段
- 使用:
- 卸载: 统计类加载耗时:反射BaseDexClassLoader的 pathList,写入自定义的 PathClassLoader(装饰者增加耗时统计)
- 类加载器 PathClassLoader:只能加载应用包内的dex
- 类加载器 DexClassLoader:可以加载任意位置的 dex
- 类加载器 BaseDexClassLoader:持有 DexPathList,结构如下, BaseDexClassLoader(DexPathList(Element数组(DexFile(多个Class))))
- DexPathList: 将 dex 文件转换成 element 存入数组(dexElements),findClass()是遍历Elements并进行类名匹配。
- Android 类加载过程:Dex 文件在类加载器中被包装成 Element,Element以数组形式被DexPathList 持有,加载类时通过遍历Element数组进行类名匹配查找,只要把新的Dex文件插入到 Element头部即可实现热修(反射)。
- 双亲委托:类加载器加载类时,将加载请求逐级向上委托,直到BootStrapClassloader,真正的加载从顶层开始,逐级向下查找。避免了类重复加载,以及安全,因为系统类总是由上层类加载器加载,无法通过自定义篡改