VirtualMemory
简述:cpu以虚拟内存,通过MMU查询 页表, 映射 高4位(页号) 到页表中查询得到 页框号 和 有效位。
如果有效位 为1, 则直接将页框号和虚拟内存低12位(偏移量)组合返回即为物理地址
如果有效位为0(意味着该页表项不存在MMU中,即未向MMU注册或相关页未被加载如内存中),则产生缺页中断,系统处理中断,通过内存置换swap算法(LRU,OPT,FIFO),之后重走一遍以上逻辑
简述:cpu以虚拟内存,通过MMU查询 页表, 映射 高4位(页号) 到页表中查询得到 页框号 和 有效位。
如果有效位 为1, 则直接将页框号和虚拟内存低12位(偏移量)组合返回即为物理地址
如果有效位为0(意味着该页表项不存在MMU中,即未向MMU注册或相关页未被加载如内存中),则产生缺页中断,系统处理中断,通过内存置换swap算法(LRU,OPT,FIFO),之后重走一遍以上逻辑
SharedPreferences
是系统提供的一种简易数据持久化的手段,适合单进程、小批量的数据存储与访问。以键值对的形式存储在xml
文件中。
文件存储路径为data/data/package_name/shared_prefs/
目录。
机制描述:
(//ps:AOSP源码中Service的前台Service的TIMEOUT时间是20s,后台Service的TIMEOUT时间是200s)
android-10
// How long we wait for a service to finish executing.
static final int SERVICE_TIMEOUT = 20*1000;
// How long we wait for a service to finish executing.
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;原理:
前台Service启动超过20s没有启动完成:在Service启动时发送一个延迟20s的消息(该消息内部即为 报ANR并分析ANR栈),之后在Service的启动完成时将这个消息remove掉。如果成功remove那就啥事没有,如果超过20s没有remove就消息触发,执行消息体内的ANR动作。(后台消息)
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。多进程同步实现是依靠文件锁
Android 存储优化 —— MMKV 集成与原理 - 掘金 (juejin.cn)
design · Tencent/MMKV Wiki (github.com)
android_ipc · Tencent/MMKV Wiki (github.com)
一些对比:
虽然 MMKV 一些场景下比 SP 稍慢(如: 首次实例化会进行数据的复写剔除重复数据, 比 SP 稍慢, 查询数据时存在 ProtocolBuffer 解码, 比 SP 稍慢), 但其逆天的数据写入速度、mmap Linux 内核保证数据的同步, 以及 ProtocolBuffer 编码带来的更小的本地存储空间占用等都是非常棒的闪光点
类的生命周期:
简述:加载是字节码(.class)文件被ClassLoader装载进方法区并在堆中生成一个class对象引用;链接包括:校验二进制流是否符合JVM规范的验证、为各个变量分配内存赋值默认值的准备、将字符串表示的符号引用解析成直接将引用的解析;初始化则是static块、static变量初始化、类构造器执行的过程。
滴滴的哆啦A梦的卡顿检测其实就是blockCanary,和Matrix 的EvilMethodTracer和AnrTracer (当然后来Matrix还增加了native的Signal信号监听)使用的 方案也就是Looper设置Printer监听卡顿
都是根据handler原理,通过给Looper.loop() 中设置printer(无论是通过反射替换Looper的mLogging还是通过setMessageLogging设置printer),监控超过 设定阈值(matrix700ms) 的主线程消息(超过5s报为ANR),printer 中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值为主线程卡慢发生,并 打印当时堆栈 + 方法耗时(matrix/dokit)
https://mp.weixin.qq.com/s/3dubi2GVW_rVFZZztCpsKg
滴滴的哆啦A梦的卡顿检测其实就是blockCanary,和Matrix 的EvilMethodTracer和AnrTracer (当然后来Matrix还增加了native的Signal信号监听)使用的 方案也就是Looper设置Printer监听卡顿
都是根据handler原理,通过给Looper.loop() 中设置printer(无论是通过反射替换Looper的mLogging还是通过setMessageLogging设置printer),监控超过 设定阈值(matrix700ms) 的主线程消息(超过5s报为ANR),printer 中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值为主线程卡慢发生,并 打印当时堆栈 + 方法耗时(matrix/dokit)
简述:
一级缓存为屏内缓存scrapView,分为没有变化的可以直接复用的ViewHolder mAttachedScrap和因notifyXXX标记为需要重新绑定的ViewHolder mChangedScrap;(用position索引)
二级缓存为离屏2个的ViewHolder离屏缓存cacheView,直接复用;(用position索引)
三级缓存为自定义缓存ViewCacheExtension,较少用
四级缓存为超出上述缓存的需要重新绑定的ViewHolder缓存池RecycledViewPool; (用viewType索引ViewHolder,每种viewType最多5个)
Glide.with(context).load(String).into(xx)
这一行代码干了多少事,其完整的流程:
Glide.with(context) 生成感知生命周期的requestManager;
1 | 1 with(context) |
RequestManager.load()确定解码类型并构建requestBuilder
1 | 2 load(string)多个重载,以String为例(没有手动调用asXXX()的情况下) |
RequestBuilder.into()确定transformations、构建ViewTarget、主线程Handler的Executor后,构建SingleRequest对象,将request对象保存到ViewTarget中,之后调用requestManager.track(target, request)中执行request.begin()开始正式加载流程。
1 | 3 into(ImageView) 多个重载,以ImageView为例 |
1 | 4 engine.load() |
在DecodeJob的getNextGenerator被执行到时,Generator会被初始化,初始化时,Generator会通过调用decodeHelper.getCacheKeys -> decodeHelper.getLoadData -> glideContext.getRegistry().getModelLoaders(model);
对 MultiModelLoaderFactory 中对 Register 注册表中已注册的所有entries的遍历,寻找与model类型匹配的Entry(包含modelClass、dataClass、具体loader的factory类)。
//注:此处的Register中可以是自己注册 model -> factory 的映射,也可能是Glide默认注册的那些。默认的model为String,启用okhttp的情况下映射到的是 OkHttpUrlLoader 的factory类。最后走OkHttpStreamFetcher请求网络
//注: String类型的model 在 StringLoader 中会先被解析成Uri,然后包装成 GlideUrl 类然后重新寻找对应的loader,也就是OkHttpUrlLoader
//注: 注册表 Register中,注册时 Key 为model的类型,value为 ModelLoaderFactory,ModelLoaderFactory build -> ModelLoader buildLoadData -> LoadData
以磁盘缓存模式为DATA_CACHE为例:
generator的执行顺序是 ResourceCacheGenerator(若有) -> DataCacheGenerator(若有) -> SourceGenerator(若不存在磁盘缓存)
第一次 decodejob 执行是在 diskCacheExecutor 线程池中
此时,首次进入runWrapped -> case INITIALIZE -> runGenerators() 时,执行的是 DataCacheGenerator.startNext(),由于此时磁盘缓存还不存在(所以也没法helper.getModelLoaders(cacheFile)),startNext返回false,走到下一个stage和generator,也就是 stageStage.SOURCE 并currentGenerator为SourceGenerator,当 stage == stageStage.SOURCE ,走到decodeJob的 reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE),此时decodejob会被加入到 sourceExecutor 线程池中重新执行。
第二次 decodejob 执行是在 sourceExecutor 线程池中
此时,重新进入runWrapped -> case SWITCH_TO_SOURCE_SERVICE -> runGenerators() 时,执行的是 SourceGenerator.startNext(),遍历当前符合的所有 ModelLoader.LoadData(其实就是一个,fetcher为OkHttpStreamFetcher的LoadData对象),最后执行loadData.fetcher.loadData() 走到 OkHttpStreamFetcher.loadData() 调用okhttp下载数据,并监听 onDataReady() 和 onLoadFaild() 回调。
onDataReady()下载完成后,数据赋值给 SourceGenerator.dataToCache ,之后又 reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE) 到sourceExecutor线程池中重新执行。
第三次 decodejob 执行依然是在 sourceExecutor 线程池中
此时,依然是runWrapped -> case SWITCH_TO_SOURCE_SERVICE -> runGenerators() ,SourceGenerator.startNext(),由于此时dataToCache不为空,走 SourceGenerator.startNext() 中先Encode数据然后存储磁盘缓存数据的逻辑,即调用 SourceGenerator.cacheData() 将源数据通过decodeHelper.getDiskCache.put(key = new DataCacheKey(loadData.sourceKey, helper.getSignature(), value = new DataCacheWriter<>(encoder, data, helper.getOptions()))写入磁盘缓存。同时构建一个 DataCacheGenerator执行其 startNext()。
DataCacheGenerator.startNext()中,此时由于已经写过磁盘缓存了,cacheFile不为空,于是走到helper.getModelLoaders(cacheFile)得到缓存文件的读取modelLoaders(此时有四个ByteBufferFileLoader,FileLoader$StreamFactory、 FileLoader$FileDescriptorFactory、UnitModelLoader)
根据当前的resourceClass和transcodeClass,以及loadData的dataClass(ByteBuffer)确定为由ByteBufferFileLoader处理,执行ByteBufferFileLoader.loadData。之后借助ByteBufferUtil类随机读取的方式,从磁盘文件中读取源数据的ByteBuffer,之后数据回调到 DecodeJob.onDataFetcherReady(),然后执行 decodeFromRetrievedData()解码源数据 (注,此时还在sourceExecutor线程池中)。
通过 这一串调用之后,走到遍历 decodePaths 尝试解析的步骤,此时一般有(AnimatedImageDecoder, ByteBufferGifDecoder, BitmapDrawableDecoder以及自定义的比如AvifBufferBitmapDecoder),任何一个decoder解码成功后即结束这段逻辑(decode时会执行Downsampler类的逻辑进行采样缩放并且解码),
重新走到 DecodeJob: decodeFromRetrievedData() -> notifyEncodeAndRelease() -> notifyComplete() ->
EngineJob: onResourceReady() -> notifyCallbacksOfResult() -> onEngineJobComplete()
此处执行活动缓存的写入逻辑,activeResources.activate(key, resource); 之后就是资源回收任务结束移除之类的收尾工作。
CallResourceReady.run() -> engineResource.acquire();callCallbackOnResourceReady(cb); -> SingleRequest.onResourceReady() -> ImageViewTarget.onResourceReady() -> ImageViewTarget.setResourceInternal() -> DrawableImageViewTarget.setResource() -> view.setImageDrawable();
此处是将解码后的图片资源直接通过view.setImageDrawable显示出来。
整体流程参考:
磁盘(DiskLruCache) -> LRUCache(不活跃资源) -> ActiveResources(使用中资源WeakReference)
内存缓存分为 ActiveResources弱引用 的和 LruCache ,其中正在使用的图片使用弱引用缓存,暂时不使用的图片用 LruCache缓存,这一点是通过 图片引用计数器(acquired变量)来实现的
内存缓存的key是由model, signature, width, height, transformations, resourceClass, transcodeClass, options
等因素共同组成的,因此,不同的宽高、变换之类的因素会生成不同的内存缓存。
活动缓存(activeResources: Map<Key, ResourceWeakReference>)
key为EngineKey对象,value为弱引用的图片缓存ResourceWeakReference实例
正在使用的图片缓存(被ImageView或Activity引用的),在gc或所在Activity(fragment宿主activity)销毁时被移到内存缓存
(为什么这一层缓存要用弱引用缓存的方式:一个是这一层缓存因为没有限制缓存大小,用弱引用如果遇到gc即可降级;一个是)
活动缓存是感知生命周期的
当绑定的context销毁onDestory时,RequestManager会将该事件传递给RequestTracker,然后触发该请求Resource的clear,再调用Engine.release,将改activiteResource降级到LruCache
LRU缓存LRUCache(memoryCache: LruCache<Key, Resource<?>>)
key为EngineKey对象,value为图片缓存Resource实例
当图片不再显示时,图片会从活动缓存移到内存缓存。存储的是暂时不用的图片缓存(依靠图片引用计数器acquired变量实现,当acquired大于0时存在于活动缓存中,为0是移到内存缓存中)
使用 LinkedHashMap实现, 存储从活跃图片降级的资源,使用Lru算法淘汰最近最少使用的
Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
存:从活跃图片降级的资源(退出当前界面,或者ActiveResource资源被回收)
取:网络请求资源之前,从缓存中取,如没命中活动资源则取活动资源,没命中活动资源则取内存缓存LRUCache,若命中则直接从LruCache中移除了,没命中则走磁盘或网络。
内存缓存最大空间(maxSize) = 每个进程可用的最大内存(activityManager.getMemoryClass() * 0.4
(低配手机的话是: 每个进程可用的最大内存 * 0.33)
activityManager.getMemoryClass值:
- 低端设备可能在 16-32 MB 范围内。
- 中档设备通常在 64-128 MB 范围内。
- 高端设备和新款设备可能在 256 MB 或更高。//当设置largeHeap时,最多可申请512M
对于远程资源:DATA/AUTOMATIC 策略下的磁盘缓存的key只由url(和signature,但一般没有)决定。
RESOURCE 策略下的磁盘缓存的key决定因素还要包括 宽高、变换等多种因素
磁盘缓存(diskLruCache: LinkedHashMap<String, Entry>)
key为 经过Sha256算法对 DataCacheKey(GlidUrl和signature)或ResourceCacheKey(GlideUrl、singnature、width、
height、transformation等多个因素) 摘要后 的64位字符串,value为File实例
DiskCacheStrategy.ALL : //表示既缓存原始图片,也缓存decode过后的图片。
DiskCacheStrategy.NONE: //表示不缓存任何内容。
DiskCacheStrategy.RESOURCE: //表示只缓存decode过后的图片,依然编码为jpeg/png等格式存储
DiskCacheStrategy.DATA: //表示只缓存原图片(decode前)。 如webp/avif
DiskCacheStrategy.AUTOMATIC //(默认选项)Remote资源下走DiskCacheStrategy.DATA,Local资源下走DiskCacheStrategy.RESOURCE。
实际是内存中解码后的Bitmap对象,编码成磁盘需要的文件格式,jpeg/png,之后再存磁盘
解码后的资源的磁盘缓存(经过转换,解压之后的体积较大,解码速度较快的,由bitmap直接编码成的JPEG/PNG等未压缩格式)
此时存储的文件是文件头为ffd8 ffe0 0010 4a46 4946 0001 0100 0001
的jpeg格式。
Resource 代表了解码后的资源,即已经从原始数据(如网络图片的字节流)解码并转换为可直接使用的数据形式,之后还是需要编码成文件格式jpeg/png等文件存储格式。Resource 封装了解码后的资源,并提供了一些管理功能,如引用计数和资源回收。
原资源的直接数据缓存(未解码,转换,磁盘体积较小,解码速度较慢)(AVIF/WEBP等压缩格式)
此时存储的文件是文件头为0000 0020 6674 7970 6176 6966 0000 0000
的avif格式。
DataCache 是指数据缓存,主要用于缓存原始数据(例如,通过网络请求获取的图片字节流)。数据缓存可以加速后续的解码过程,因为原始数据已经被缓存下来,不需要再次从网络或其他源获取。
Remote资源下走DiskCacheStrategy.DATA,Local资源下走DiskCacheStrategy.RESOURCE。
对于远程的资源,DATA和AUTOMATIC是一样的。
想缩短解码时间可以考虑使用RESOURCE模式,本质上是用磁盘空间 换 解码时间:decode后的资源存储空间更大,但解码时间更短。
对于webp,avif等模式,RESOURCE缓存的是.jpeg格式,其内容是decode后内存中使用的bitmap对象直接,不带压缩的二进制内容。DATA缓存的则是原始的图片格式
**那么对于.jpg文件,RESOURCE跟DATA模式的区别是啥:jpg本身也是有压缩的,RESOURCE模式下存储的文件格式仍为jpg,但是其内容是decode后的,尺寸更大而解码时间更短 **
那么对于.gif文件,不管什么模式都一样,就是缓存gif图原始文件
互联网早期图片现在看到很多事绿色且模糊,其实就是由于jpeg的压缩是有损的,每次存到磁盘中可能经过一次压缩,并且上传时会再压缩一次,重复多次后就会质量下降
Glide生成key(两级内存缓存Key类型为同一个EngineKey对象,磁盘缓存Key类型为DataCacheKey或ResourceCacheKey摘要的哈希字符串)的方式涉及的参数有8种,其中都包括图片URL、签名。可能包括宽高、变换等
1 | EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, |
宽高一定是明确有值的。即使wrap_content也会用屏幕最长的一边作为兜底宽高。
内存缓存最大空间(maxSize) = 每个进程可用的最大内存(activityManager.getMemoryClass() * 0.4
(低配手机的话是: 每个进程可用的最大内存 * 0.33)
activityManager.getMemoryClass值:
- 低端设备可能在 16-32 MB 范围内。
- 中档设备通常在 64-128 MB 范围内。
- 高端设备和新款设备可能在 256 MB 或更高。//当设置largeHeap时,最多可申请512M
磁盘缓存大小: 默认250MB
磁盘缓存目录: 项目/cache/image_manager_disk_cache
这里有个点:随着图片磁盘缓存的存取,由于每次存取磁盘图片glide都会将存取操作记录到日志文件(journal文件),日志文件会逐渐增大到可能几兆大小,导致glide初始化延迟(glide的初始化中,磁盘缓存初始化时会涉及到将日志文件读取到内存中操作)。
这里可以增加一个首页模块的磁盘缓存目录,比如项目/cache/image_manager_disk_home_cache
,启动时先初始化首页的磁盘缓存(现为10m)再初始化主体的磁盘缓存,避免日志文件过大导致的初始化时间太长阻塞App启动。
通过链式调用中with(xxx)传入的activity或Fragment,Glide实现了对所属activity或者fragment生命周期监听:通过new一个隐形的fragment(SupportRequestManagerFragment.class),嵌入到所要监听的fragment或者activity所在的Activity中,这样当宿主Acitivity的生命周期变化时,可以通过嵌入的fragment监听回调,并在RequestManager中执行:
**onStart(): 继续请求resumeRequests() **
onStop(): 暂停请求pauseRequests()
onDestory(): 销毁请求、对应页面的活动缓存降级到内存缓存LRUCache,移除监听等操作
// ps:当 Activity时,Glide 会清理相关资源,移除 Fragment 不会立即触发 ActiveResources 缓存降级,只有宿主 Activity 销毁时才会。
//com/bumptech/glide/request/target/CustomViewTarget.java 中保留了一个没打开的接口clearOnDetach(),可以实现当ImageView detachedFromWindow的时候释放图片缓存。但由于考虑到太激进的释放可能导致缓存复用效率
如果view的布局宽高有值,或view本身的宽高有值,会直接返回布局宽高或view宽高。如果没有:
ViewTarget会为所持有的View注册view树绘制回调,待到经过测量布局之后回调onSizeReady()了,才会发起engine.load。(如果wrap_content则会用getMaxDisplayLength()屏幕长的一边作为宽高返回)
addOnPreDrawListener(OnPreDrawListener listener)
那么会返回 屏幕高(长的一边) * 屏幕高,比如 1080 * 1920 屏幕会返回 1920 * 1920兜底,在后续流程比如内存缓存Key生成时候,也是用的这个兜底宽高。
具体见ViewTarget.getMaxDisplayLength()
注册ComponentCallbacks2,实现细粒度内存管理:
1 | memoryCache.trimMemory(level); // 内存缓存 |
可以设置在onTrimMemory时,取消所有正在进行的请求。
(触发下载时存取磁盘和解析使用的线程池,非下载线程池,下载包给okhttp/httpurlconnection了):
定长为 cpu数量(最大4)(核心线程和工作线程数量都最大为4)的线程池
1 | private GlideExecutor getActiveSourceExecutor() { |
(从Resource或DataCache中加载图片)线程池:定长为1(核心线程和工作线程数量都为1)的线程池
//diskCacheExecutor
//DEFAULT_DISK_CACHE_EXECUTOR_THREADS = 1
public static GlideExecutor newDiskCacheExecutor() {
return newDiskCacheExecutor(
DEFAULT_DISK_CACHE_EXECUTOR_THREADS,
DEFAULT_DISK_CACHE_EXECUTOR_NAME,
UncaughtThrowableStrategy.DEFAULT);
}
读取流的前三个字节,若判断是gif,则会命中gif解码器-将资源解码成GifDrawable,它持有GifFrameLoader会将资源解码成一张张Bitmap并且传递给DelayTarget的对象,该对象每次资源加载完毕都会通过handler发送延迟消息回调 onFrameReady() 以触发GifDrawable.invalidataSelf()重绘。加载下一帧时会重新构建DelayTarget
Q: glide 当前页面的图片缓存是存在activiteResource的弱引用的,也就是说当gc时当前页面的图片缓存会被回收?那岂不是当前页面会空白
A: 不会啊,因为实际的资源被页面的view持有着,而ActiveResources只是一层弱引用而已。而已经不被view持有(没显示出来的图片)但仍在活动缓存中的弱引用会被降级到LRU内存缓存中。
活动资源缓存的存在以较小的代价减小Lru缓存的压力,提升Lru缓存的效率。
原因是活动资源缓存通过缓存的对象本身就是在内存中进行使用,缓存是只是建立一个弱引用关系。如果过没有活动资源缓存,每一次使用的资源都加入内存缓存,极有可能因为放入Lru缓存的数据过多,导致正在使用资源从Lru缓存中移除,等到下次来进行加载的时候因为没有对应的引用关系,找不到原来内存中正在使用的那个资源,从而需要再次从文件或者网络进行数据加载。这样同一份资源需要使用两处或者多处内存。大大的提高了内存消耗。总而言之,活动资源缓存以较小的代价提高了Lru缓存的使用效率,防止加载中的资源被lru回收。
1 | RequestBuilder.java |
1 | SingleRequest.java |
1 | Engine.java |
1 | Engine.java { |
Resource 代表了解码后的资源,即已经从原始数据(如网络图片的字节流,一般为AVIF、WEBP)解码并转换为可直接使用的数据形式(如 Bitmap、Drawable 等,如果存到磁盘会encode成jpeg/png格式)。Resource 封装了解码后的资源,并提供了一些管理功能,如引用计数和资源回收。
DataCache 是指数据缓存,主要用于缓存原始数据(例如,通过网络请求获取的图片字节流)。数据缓存可以加速后续的解码过程,因为原始数据已经被缓存下来,不需要再次从网络或其他源获取。
Source 代表数据的源头,即原始数据的来源。常见的源包括网络(主要)、文件系统、资源文件、内容提供者等。
网络源:从 URL 或网络地址加载数据。
文件源:从本地文件系统加载数据。
资源源:从应用的资源文件中加载数据。
内容提供者:通过内容提供者(ContentProvider)加载数据。
resourceClass //图片以什么类型解码图片资源
resourceClass 是指图像加载过程中,Glide 从源(如网络或本地存储)加载并解码后的资源类型。它表示 Glide 获取图像数据后将其解码成什么类型的资源。
常见的 resourceClass 类型包括:
- Bitmap:用于静态图像,如 JPEG、PNG。
- GifDrawable:用于 GIF 动图。
- Drawable:一个通用类型,可以表示 BitmapDrawable、GifDrawable 等。
指定 resourceClass 可以确保 Glide 以指定的方式解码和处理图像资源。例如:
1
2
3
4
5
6 java
Copy
Glide.with(context)
.asBitmap() // 指定resourceClass为 Bitmap。不指定时默认asDrawable为drawable
.load("https://example.com/image.jpg")
.into(imageView);transcodeClass // 最终提供给 Target(如 ImageView)的数据类型
transcodeClass 是指从 resourceClass 类型转换后的最终数据类型,它表示图像加载和解码后,最终提供给 Target(如 ImageView)的数据类型。
常见的 transcodeClass 类型包括:
- Drawable:Glide 默认使用 Drawable 作为最终显示的类型,因为它可以表示不同类型的图像资源。
- Bitmap:如果你需要直接处理 Bitmap 对象,可以指定 transcodeClass 为 Bitmap。
- 自定义类型:你可以定义自己的转换器,将资源转换为自定义类型。
通过自定义转换器(ResourceTranscoder),你可以将 resourceClass 类型转换为所需的 transcodeClass 类型。例如,如果你想将 GIF 转换为静态图像或者其他类型,可以使用自定义转换器。
总结
- resourceClass:表示 Glide 从源加载并解码后的资源类型(如 Bitmap、GifDrawable)。
- transcodeClass:表示从 resourceClass 类型转换后的最终数据类型,可以是 Drawable、Bitmap 或其他自定义类型。
PackageManagerService
WindowManagerService(done?)
ActivityManagerService(done?)
ServiceManager(done?)
recyclerview缓存和优化
app、android启动流程
编译流程
编舞者以及屏幕刷新原理(与耗时方法监听之)
插件化(代理和hook两种方式)
handler
https&http(http version)
hashmap
Matrix为主,Dokit、blockcanary等APM框架
Jmm与GC算法(深入理解java虚拟机-标记清除之类)
tcp滑动窗口之类(就找一篇文章like腾讯之前的那篇tcp ip问题彻底弄懂TCP协议:从三次握手说起 (qq.com))
leakCanary、retrofit、okhttp、glide、
threadLocal、Rv四级缓存
图片内存优化工作:fresco关于gif缓存问题修复,dokit图片闪烁缓存失效问题的修复、基于dokit框架(ASM)下尺寸过大图片识别实现。(基于业务做的一些图片加载的优化,如cdn链接统一域名,regex(String.replace)耗时200us)
耗时方法,两种常见的监听方式:looper.printer 以及 编舞者回调监听
recyclerview优化
ps:我并不想抽象的去将购物车的架构之类的东西,而是更具体的讲遇到的问题及解决方案。不过还是得简单讲一下设计
购物车代码十分屎山,难以维护。在23年时做了一次历经一个月的重构,边开车边换轮子。使用多数业务在用的协议架构奥创重构:先说收益:奥创加购以组件为单位,职责清晰,易于复用维护,端侧组件使用MVVM架构进一步解耦逻辑:
举个例子,请求在model层(共用的Repository,维护于Engine层),每个组件通过注册组件的ViewHolder获得独立的View,注册组件的Parser获得独立的ViewModel解析数据及进行业务逻辑处理。view与vm之间使用liveData进行数据更新后的通知view状态渲染。
遇到的问题:
做过印象比较深的事情 ,有挑战的事情
弱网环境优化:业务和技术的手段,有什么很技术的手段来做吗
当前团队的优劣点
遇到不合理需求你会怎么拒绝
有用过新的东西吗
线上问题怎么解决,因为不能用动态字节码技术
遇到啥有意思的问题吗,crash。window
1 | java.lang.IllegalArgumentException: View=android.widget.PopupWindow$PopupDecorView{2a10009 V.E...... R.....I. 0,0-0,0} not attached to window manager |
OOM的治理:crash率在一次架构组的升级后由万4涨到万6,其中新增了很多的OOM。1:修复了EventCenter的内存泄露;2:搜推服务造成的泄露;
内存兜底措施:2g设备直接改为RGB565,其他才开ARGB8888;同时降低起设备图片内存池大小;
equals()
hashCode()
1 | public int hashCode() { |
toString()
copy()
componentN()
编译器会为数据类生成 组件函数(Component function), 有了这些组件函数, 就可以在 解构声明(destructuring declaration) 中使用数据类:
1 | val jane = User("Jane", 35) |
属性的get()/set()
val的属性不会有setter
constructor()
只有有参构造函数,没有无参构造函数。fastJson解析会抛该异常,需升级到高版本并引入kotlin-reflect依赖
简述:
开机后系统将Rom文件加载进Ram内存中,loader检查Ram,kernel启动Swapper进程和kthreadd进程(创建内核守护进程)。
NativeFramework中init.cpp运行后启动init进程,init进程解析init.rc文件后,孵化如installd、logd、adbd等用户守护进程、启动servicemanager、surfaceflinger、bootanim等服务、孵化出Zygote虚拟机进程(java进程)。
JavaFramework中Zygote注册ZygoteSocket、加载虚拟机、预加载通用类、资源;之后孵化system_server进程,启动如AMS(startService可监听RootPhase)、WMS、PKMS、PMS等服务。
App:由Zygote孵化的第一个App——Launcher(桌面),用户点击Launcher上的app图片,通过JNI调用AMS从Zygote进程中fork出新的App。
插桩:通过插桩,在除了get/set、默认或匿名构造函数等简单函数外的所有方法,入口/出口插入MethodBeat.i()/MethodBeat.o()。
1 | //AppMethodBeat.java |
用一个64位的long型来存储方法的:方法开始/方法结束(最高位63位)、方法id(递增,43到62位)、当前与MethodBeat模块初始化时差(0到42位)
事件分发中有一个重要的规则:一个触控点的一个事件序列只能给一个view处理
分析:以DOWN事件为序列分发判定,ViewGroup为消费DOWN事件的View生成一个TouchTarget(这个TouchTarget就包含了该view的实例与触控id,id可以是多个以应对多指触控),后续MOVE、UP都会交给这个TouchTarget。如果TouchTarget为空则ViewGroup自己处理。如果viewGroup消费了down事件,那么子view将无法收到任何事件。
插件化和热修复不是同一个概念,虽然站在技术实现的角度来说,他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;但是二者的出发点是不同的。插件化顾名思义,更多是想把需要实现的模块或功能当做一个独立的提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。热修复则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。
热知识:java常见的虚拟机如Hotspot虚拟机是基于栈结构的,而Dalvik是基于寄存器结构的。
常见的java虚拟机跑的是.class文件,而Dalvik跑的是.dex(.odex)文件。
BoostMultiDex优化Dalvik虚拟机多Dex启动速度
Android 4.4 及以下采用的是 Dalvik 虚拟机,在通常情况下,Dalvik 虚拟机只能执行做过 OPT 优化的 DEX 文件,也就是我们常说的 ODEX 文件。
一个 APK 在安装的时候,其中的classes.dex会自动做 ODEX 优化,并在启动的时候由系统默认直接加载到 APP 的PathClassLoader里面,因此classes.dex中的类肯定能直接访问,不需要我们操心。
除它之外的 DEX 文件,也就是classes2.dex、classes3.dex、classes4.dex等 DEX 文件(这里我们统称为 Secondary DEX 文件),这些文件都需要靠我们自己进行 ODEX 优化,并加载到 ClassLoader 里,才能正常使用其中的类。否则在访问这些类的时候,就会抛出ClassNotFound异常从而引起崩溃。
因此,Android 官方推出了 MultiDex 方案。只需要在 APP 程序执行最早的入口,也就是Application.attachBaseContext里面直接调MultiDex.install,它会解开 APK 包,对第二个以后的 DEX 文件做 ODEX 优化并加载。这样,带有多个 DEX 文件的 APK 就可以顺利执行下去了。
这个操作会在 APP 安装或者更新后首次冷启动的时候发生,正是由于这个过程耗时漫长,才导致了我们最开始提到的耗时黑屏问题。
1 | if (Build.VERSION.SDK_INT <= 19) { |
*Retrofit通过 反射构建一个 接口的 实现类(动态代理本质就是反射),其中每个重写方法被调用时,都会回调到InvocationHandler.invoke中,invoke回调时(只要是不是object类中的方法)都会通过 获取到的方法的注解、方法的名称、方法的返回值、方法参数的注解、方法参数类型等等所需信息, 解析成一个ServiceMethod对象(放入缓存池),ServiceMethod根据获取到的方法信息,构建OkHttp请求,并将结果通过converter转换后回调给最初传入的Callback
链接:https://juejin.cn/post/6887896333685161992
简述:
通过对外提供的OkHttpClient和Request的builder实现基础信息和必要信息的配置,直到封装构建成了RealCall对象(RealCall implement Call)并新建CallBack实例传入realCall.equeue(callback),才真正完成了请求实体的实例化。
之后realCall.enqueue(call)方法的调用才是实际上开始进行请求:先判断是否call已经执行过了(executed = AtomicBoolean()),若未执行则继续
之后由Dispatcher调用enqueue进行判断当前正在执行的请求数及当前网络请求的主机数是否超过了最大值。要是超过了最大值,就将请求放到等待队列中,要是没超过,就放到正在执行的队列中,然后调用线程池(默认单例初始化了一个缓存线程池(即无核心线程、无限线程池数量、SynchronousQueue
线程栈中的局部变量表引用的所有变量,即运行线程中引用到的所有变量,包括线程中方法参数和局部变量
存活的线程对象
native 的 jni引用
class 对象 (classLoader 不会卸载class)
引用类型的静态变量
// 1跟2其实说的是一个东西
Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.
在检测到适当的可达性改变后,垃圾收集器将注册的引用对象(WeakReference)追加到引用队列(ReferenceQueue)。
核心思路是:
leakCanary做法是ondestory后手动出发GC,GC过后对象WeakReference一直不被加入 ReferenceQueue,它可能存在内存泄漏。
利用 双参初始化的弱引用 WeakReference(T referent, ReferenceQueue<? super T> q) 在object对象变成弱可达的时候(可视为已被回收/(其他非强引用也一样)),会将该WeakReference对象入队q中 的特性(也就是queque中最后会存在已经被回收了的weakreference对象),通过在Activity和Fragment的onDestroy()中,将该Activity或Fragment实例的弱引用初始化(双参object,queue)后放入map中(key随机固定uuid),GC后 把map中的 queue包含的对象 移除,map中剩余的即为可能泄露的对象
Android 2.3 之前: 像素数据存在于native heap
Android 3.0 ~ 7.1 之间: 存在于 java heap
Android 8.0及之后: 存在于 native
Aapt 会将主工程、依赖库中的资源(res、assets)和androidManifest都合并,产出R.java、资源及资源索引resources.arsc;
之后javac编译包括R.java文件、主工程的java文件、aidl产生的java文件,产出class文件;如果需要插桩的话就插桩
之后使用Proguard/R8混淆工具对.class文件脱糖、压缩、混淆等,产出新的class文件;
之后使用Dx/D8编译工具将新的class文件再转换成dex文件,
之后打包成apk,然后签名、zipalign优化。
工具:aapt/aapt2、javac、Proguard/R8、Dx/D8、ApkBuilder、zipalign
当手指点击了桌面的App图标时发生了什么 - ProcessOn
主要参考 https://juejin.cn/post/6863756420380196877#heading-12
省流版:
双缓存:为了解决画面撕裂;画面撕裂来自于只有一个buffer时,正在display的那一帧数据被后一帧的数据覆盖了
Vsync:系统在收到VSync pulse(Vsync脉冲)后,将马上开始下一帧的渲染,(CPU开始计算数据)。
三缓冲:当显示器正在写入FrameBuffer同时GPU也正在写入BackBuffer时,下一次渲染开始了,此时CPU可以使用新增的GraphicBuffer进行计算。减少了Jank。(更多缓冲需要耗费更大的内存)
ChoreoGrapher机制:规定了数据计算开始(measure、layout、draw)的时机(vsync信号),使计算到渲染图像数据能有一个完整的16.6ms:更新ui(request()/invalidate())后编舞者注册vsync信号回调,在下一个vsync信号到时候立刻进行view的测量布局绘制