VirtualMemory

img

简述:cpu以虚拟内存,通过MMU查询 页表, 映射 高4位(页号) 到页表中查询得到 页框号 和 有效位。

如果有效位 为1, 则直接将页框号和虚拟内存低12位(偏移量)组合返回即为物理地址

如果有效位为0(意味着该页表项不存在MMU中,即未向MMU注册或相关页未被加载如内存中),则产生缺页中断,系统处理中断,通过内存置换swap算法(LRU,OPT,FIFO),之后重走一遍以上逻辑

Read more

sharePreference

SharedPreferences原理-xmind

SharedPreferences原理-xmind

SharedPreferences是系统提供的一种简易数据持久化的手段,适合单进程、小批量的数据存储与访问。以键值对的形式存储在xml文件中。
文件存储路径为data/data/package_name/shared_prefs/目录。

源码解析

源码解析

源码解析

Read more

ANR

机制描述:

图片

(//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:

前台Service启动超过20s没有启动完成:在Service启动时发送一个延迟20s的消息(该消息内部即为 报ANR并分析ANR栈),之后在Service的启动完成时将这个消息remove掉。如果成功remove那就啥事没有,如果超过20s没有remove就消息触发,执行消息体内的ANR动作。(后台消息)

Read more

mmkv

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 编码带来的更小的本地存储空间占用等都是非常棒的闪光点

Read more

ClassLoading

1041642665584_.pic_hd_副本

类的生命周期:

classloadinglifecycle

简述:加载是字节码(.class)文件被ClassLoader装载进方法区并在堆中生成一个class对象引用;链接包括:校验二进制流是否符合JVM规范的验证、为各个变量分配内存赋值默认值的准备、将字符串表示的符号引用解析成直接将引用的解析;初始化则是static块、static变量初始化、类构造器执行的过程。

Read more

APM

主线程卡顿监控

方案一、Looper Printer监控每次 dispatchMessage 的执行耗时:

DoKit & BlockCanary & Matrix

滴滴的哆啦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)

Read more

卡顿监控

https://mp.weixin.qq.com/s/3dubi2GVW_rVFZZztCpsKg

主线程卡顿监控

方案一、Looper Printer监控每次 dispatchMessage 的执行耗时:

DoKit & BlockCanary & Matrix

滴滴的哆啦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)

Read more

recyclerView

缓存机制

简述:

一级缓存为屏内缓存scrapView,分为没有变化的可以直接复用的ViewHolder mAttachedScrap和因notifyXXX标记为需要重新绑定的ViewHolder mChangedScrap;(用position索引)

二级缓存为离屏2个的ViewHolder离屏缓存cacheView,直接复用;(用position索引)

三级缓存为自定义缓存ViewCacheExtension,较少用

四级缓存为超出上述缓存的需要重新绑定的ViewHolder缓存池RecycledViewPool; (用viewType索引ViewHolder,每种viewType最多5个)

Read more

Glide

Glide加载图片流程

Glide.with(context).load(String).into(xx)

这一行代码干了多少事,其完整的流程:

With()

Glide.with(context) 生成感知生命周期的requestManager;

1
2
3
1 with(context) 
调用getRetriever(activity).get(activity);
通过获取RequestManagerRetriever,拿到其fragmentManager(如果context是fragment的话拿其宿主Activity的fragmentManager),向其中塞一个隐性fragment,用于监听生命周期的变化,并在暂停、停止、销毁时执行对应的行为

Load()

RequestManager.load()确定解码类型并构建requestBuilder

1
2
3
4
5
6
7
2 load(string)多个重载,以String为例(没有手动调用asXXX()的情况下)
2.1 调用asDrawable().load(string),先asDrawable设定resourceClass类型为Drawable.class,再调用
load(String) {
this.model = model;
isModelSet = true;
}
设定load模型 (model)

Into()

image-20241017165028811

RequestBuilder.into()确定transformations、构建ViewTarget、主线程Handler的Executor后,构建SingleRequest对象,将request对象保存到ViewTarget中,之后调用requestManager.track(target, request)中执行request.begin()开始正式加载流程。

Into(ImageView)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
3 into(ImageView) 多个重载,以ImageView为例
3.0.1 clone一份当前的requestOption,根据scaleType(默认为FIT_CENTER)通过requestOptions.clone().optionalFitCenter()保存一些transformations和Options

3.0.2
into(
glideContext.buildImageViewTarget(view, transcodeClass), // transcodeClass为Drawable.class(默认),构建一个DrawableImageViewTarget对象(extends ImageViewTarget extends ViewTarget),ViewTarget会持有ImageView
/*targetListener=*/ null,
requestOptions, // 3.0.1中clone出来的requestOptions对象,其中保存了一些transformation配置
Executors.mainThreadExecutor()); // 主线程Handler的Executor,传递给engine使用,在job完成时callback回主线程

3.1 buildRequest(target, targetListener, options, callbackExecutor); //根据3.0.2构建的Target、requestOptions、targetListener为null、主线程回调的Executor,构建SingleRequest对象

3.2 target.setRequest(request); // target.setTag() 调用View.Tag,将3.1构建的request存到View中

3.3 requestManager.track(target, request); // 主要是调用requestTracker.runRequest(request) -> request.begin();开始加载流程

3.4 request.begin()
3.4.0 如果model为空,直接set error(优先)或者Placeholder返回
3.4.1 如果这时候能拿到target(ImageView)的宽高就走onSizeReady()开始engine.load()
3.4.2 如果这时候宽高还未测量完成,则走target.getSize(this)注册一个ViewTreeObserver,在addOnPreDrawListener回调回来后再掉onSizeReady()
3.4.3 置状态为RUNNING,status = Status.RUNNING;
3.4.4 engine.load()启动 尝试读取缓存、下载、解码等步骤,见4详解
3.5 target.onLoadStarted(getPlaceholderDrawable()); 加载占位图

Engine.load()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
4 engine.load()

4.1 先读内存缓存
内存缓存:先读 activeResource,再读memoryCache

4.2 内存缓存都没有,就走Engine job启动decode job的流程:engineJob.start(decodeJob);
4.2.1 engineJob和decodeJob都是通过池化技术(实现Poolable接口)获取,减少创建带来的内存和时间消耗

4.3 engineJob.start会在磁盘线程池diskCacheExecutor中执行decodeJob的run()方法(允许磁盘缓存情况下RESOURCE_CACHE/DATA_CACHE)
4.3.1 run()->runWrapped()中通过runGenerators()执行状态机
将根据缓存策略依次执行
ResourceCacheGenerator (缓存策略为ALL、AUTOMATIC、RESOURCE,即该策略下decodeCachedResource()返回为true)
-> DataCacheGenerator (缓存策略为ALL、AUTOMATIC、DATA,即该策略下decodeCachedData()返回为true)
-> SourceGenerator (若当前禁止磁盘缓存或磁盘缓存不存在)

private void runGenerators() {
currentThread = Thread.currentThread();
startFetchTime = LogTime.getLogTime();
boolean isStarted = false;
while (!isCancelled
&& currentGenerator != null
&& !(isStarted = currentGenerator.startNext())) {
stage = getNextStage(stage);
currentGenerator = getNextGenerator();

if (stage == Stage.SOURCE) {
reschedule();
return;
}
}
// We've run out of stages and generators, give up.
if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
notifyFailed();
}

// Otherwise a generator started a new load and we expect to be called back in
// onDataFetcherReady.
}
private Stage getNextStage(Stage current) {
switch (current) {
case INITIALIZE:
return diskCacheStrategy.decodeCachedResource()
? Stage.RESOURCE_CACHE
: getNextStage(Stage.RESOURCE_CACHE);
case RESOURCE_CACHE:
return diskCacheStrategy.decodeCachedData()
? Stage.DATA_CACHE
: getNextStage(Stage.DATA_CACHE);
case DATA_CACHE:
// Skip loading from source if the user opted to only retrieve the resource from cache.
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: " + current);
}
}
private DataFetcherGenerator getNextGenerator() {
switch (stage) {
case RESOURCE_CACHE:
return new ResourceCacheGenerator(decodeHelper, this);
case DATA_CACHE:
return new DataCacheGenerator(decodeHelper, this);
case SOURCE:
return new SourceGenerator(decodeHelper, this);
case FINISHED:
return null;
default:
throw new IllegalStateException("Unrecognized stage: " + stage);
}
}


4.3.2 DataFetcherGenerator的执行流程

没有缓存的情况举例:
4.3.2.1 通过状态机初始状态下,第一次进到DataCacheGenerator,由于不存在缓存,decodeJob会被重新加到sourceExecutor中执行,来到SourceGenerator.

4.3.2.2 SourceGenerator.startNext()中遍历当前注册表已注册的loader,glideContext.getRegistry().getModelLoaders,依次尝试寻找对应的ModelLoader:
匹配到的是GlideUrl.class对应的 OkHttpUrlLoader -> OkHttpStreamFetcher,调用okhttp3
(默认其实是 HttpGlideUrlLoader -> HttpUrlFetcher,调用HttpURLConnection的API)

4.3.2.2.1 请求接口返回回来之后,赋值变量 dataToCache 同时调用cb(DecodeJob).reschedule();

4.3.2.2.2 赋值runReason为SWITCH_TO_SOURCE_SERVICE 同时调用callback(EngineJob).reschedule(this)//decodeJob,将解析任务decodejob加入getActiveSourceExecutor中执行。

4.3.2.3 之后就又走到了SourceGenerator.startNext(),
区别是这次runWrapped()中runReason是SWITCH_TO_SOURCE_SERVICE同时SourceGenerator的dataToCache不为空。
于是又走SourceGenerator.startNext()中cacheData(data);的逻辑,将接口返回源数据后写入磁盘缓存

4.3.2.4 之后主动执行DataCacheGenerator.startNext()进行源数据磁盘缓存读取操作,此时通过匹配到的 ByteBufferFileLoader.loadData进行文件随机读取操作从磁盘文件中读取出数据。

4.3.2.5 读取到源数据之后通过DecodeJob.onDataFetcherReady将源数据回到到DecodeJob中执行Decode
解码操作
4.3.2.6 解码后又通过EngineJob的回调,执行activeResouce缓存写入和任务资源之类的回收操作
4.3.2.7 同时通过SingleRequest的回调,调用Target的setResource即最后调用ImageView.setImageDrawable

Generator具体执行流程

在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线程池中)。
通过 20241009145727 这一串调用之后,走到遍历 decodePaths 尝试解析的步骤,此时一般有(AnimatedImageDecoder, ByteBufferGifDecoder, BitmapDrawableDecoder以及自定义的比如AvifBufferBitmapDecoder),任何一个decoder解码成功后即结束这段逻辑(decode时会执行Downsampler类的逻辑进行采样缩放并且解码),

image-20241009174652194

重新走到 DecodeJob: decodeFromRetrievedData() -> notifyEncodeAndRelease() -> notifyComplete() ->

  1. EngineJob: onResourceReady() -> notifyCallbacksOfResult() -> onEngineJobComplete()
    此处执行活动缓存的写入逻辑,activeResources.activate(key, resource); 之后就是资源回收任务结束移除之类的收尾工作。

  2. CallResourceReady.run() -> engineResource.acquire();callCallbackOnResourceReady(cb); -> SingleRequest.onResourceReady() -> ImageViewTarget.onResourceReady() -> ImageViewTarget.setResourceInternal() -> DrawableImageViewTarget.setResource() -> view.setImageDrawable();
    此处是将解码后的图片资源直接通过view.setImageDrawable显示出来。

Tips

  • Glide是先将图片下载到磁盘,然后再从磁盘中读取。之后decode使用
  • 不存在磁盘缓存需要fetch的情况下:存储和读取磁盘数据,包括后续的磁盘取出后解码操作 是在sourceExecutor线程池中进行的(即是都在上述的第三步中完成)
  • 存在磁盘缓存的情况下:读取磁盘数据和解码数据 是在diskCacheExecutor线程池中进行的。

整体流程参考:

img

Glide缓存

三级缓存:

磁盘(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即可降级;一个是)

  • 资源被包装成带引用计数的EngineResource,标记引用资源的次数(当引用数acquired不为0时阻止被回收或降级,降级即是存储到LruCache中)
  • 这一级缓存没有大小限制,所以使用了资源的弱引用
  • 存:每当下载资源后会在onEngineJobComplete()中存入ActiveResource,或者LruCache命中后,将资源从中LruCache移除并存入ActiveResource。
  • 取:每当资源释放时,会降级到LruCache中(请求对应的context onDestroy了或者被gc了)
  • 开一个后台线程,监听ReferenceQueue,不停地从中获取被gc的缓存,将其从ActiveResource中移除,并重新构建一个新资源将其降级为LruCache(这一步并不会影响页面展示,图片内存没有销毁,只是被降级到有大小限制的LruCache中)
  • ActiveResource是为了缓解LruCache中缓存造成压力,因为LruCache中没有命中的缓存只有等到容量超限时才会被清除,强引用即使内存吃紧也不会被gc,现在当LruCache命中后移到ActiveResource,弱引用持有,当内存吃紧时能被回收。

活动缓存是感知生命周期的

​ 当绑定的context销毁onDestory时,RequestManager会将该事件传递给RequestTracker,然后触发该请求Resource的clear,再调用Engine.release,将改activiteResource降级到LruCache

LRU缓存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 Value:

key为 经过Sha256算法对 DataCacheKey(GlidUrl和signature)或ResourceCacheKey(GlideUrl、singnature、width、

height、transformation等多个因素) 摘要后 的64位字符串,value为File实例

  1. 在内存中用LinkedHashMap实现的LruCache记录一组Entry,Entry内部包含一组文件,文件名即是key,并且有开启后台线程执行删除文件操作以控制磁盘缓存大小
  2. 写磁盘缓存即是触发Writer将数据写入磁盘,并在内存构建对应的File缓存在LinkedHashMap中
  3. 磁盘缓存的存储顺序记录在journal文件中,有时该文件过大会导致glide初始过慢。
  • 磁盘缓存大小默认250MB,根据缓存策略的不同可能存储原始图片或解码后的图片。

DiskCacheStrategy.ALL : //表示既缓存原始图片,也缓存decode过后的图片。

DiskCacheStrategy.NONE: //表示不缓存任何内容。

DiskCacheStrategy.RESOURCE: //表示只缓存decode过后的图片,依然编码为jpeg/png等格式存储

DiskCacheStrategy.DATA: //表示只缓存原图片(decode前)。 如webp/avif

DiskCacheStrategy.AUTOMATIC //(默认选项)Remote资源下走DiskCacheStrategy.DATA,Local资源下走DiskCacheStrategy.RESOURCE。

磁盘-RESOURCE

实际是内存中解码后的Bitmap对象,编码成磁盘需要的文件格式,jpeg/png,之后再存磁盘

解码后的资源的磁盘缓存经过转换,解压之后的体积较大,解码速度较快的,由bitmap直接编码成的JPEG/PNG等未压缩格式

此时存储的文件是文件头为ffd8 ffe0 0010 4a46 4946 0001 0100 0001的jpeg格式。

Resource 代表了解码后的资源,即已经从原始数据(如网络图片的字节流)解码并转换为可直接使用的数据形式,之后还是需要编码成文件格式jpeg/png等文件存储格式。Resource 封装了解码后的资源,并提供了一些管理功能,如引用计数和资源回收。

磁盘-DATA

原资源的直接数据缓存(未解码,转换,磁盘体积较小,解码速度较慢)(AVIF/WEBP等压缩格式)

此时存储的文件是文件头为0000 0020 6674 7970 6176 6966 0000 0000的avif格式。

DataCache 是指数据缓存,主要用于缓存原始数据(例如,通过网络请求获取的图片字节流)。数据缓存可以加速后续的解码过程,因为原始数据已经被缓存下来,不需要再次从网络或其他源获取。

磁盘-AUTOMATIC

Remote资源下走DiskCacheStrategy.DATA,Local资源下走DiskCacheStrategy.RESOURCE

Tips

  • 对于远程的资源,DATA和AUTOMATIC是一样的。

  • 想缩短解码时间可以考虑使用RESOURCE模式,本质上是用磁盘空间 换 解码时间:decode后的资源存储空间更大,但解码时间更短。

  • 对于webp,avif等模式,RESOURCE缓存的是.jpeg格式,其内容是decode后内存中使用的bitmap对象直接,不带压缩的二进制内容。DATA缓存的则是原始的图片格式

  • **那么对于.jpg文件,RESOURCE跟DATA模式的区别是啥:jpg本身也是有压缩的,RESOURCE模式下存储的文件格式仍为jpg,但是其内容是decode后的,尺寸更大而解码时间更短 **

  • 那么对于.gif文件,不管什么模式都一样,就是缓存gif图原始文件

  • 互联网早期图片现在看到很多事绿色且模糊,其实就是由于jpeg的压缩是有损的,每次存到磁盘中可能经过一次压缩,并且上传时会再压缩一次,重复多次后就会质量下降

Glide缓存Key

Glide生成key(两级内存缓存Key类型为同一个EngineKey对象,磁盘缓存Key类型为DataCacheKey或ResourceCacheKey摘要的哈希字符串)的方式涉及的参数有8种,其中都包括图片URL、签名。可能包括宽高、变换等

1
2
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);

宽高一定是明确有值的。即使wrap_content也会用屏幕最长的一边作为兜底宽高。

Glide的缓存大小默认是多少

内存缓存大小

内存缓存最大空间(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

image-20240815114741925

这里有个点:随着图片磁盘缓存的存取,由于每次存取磁盘图片glide都会将存取操作记录到日志文件(journal文件),日志文件会逐渐增大到可能几兆大小,导致glide初始化延迟(glide的初始化中,磁盘缓存初始化时会涉及到将日志文件读取到内存中操作)。

这里可以增加一个首页模块的磁盘缓存目录,比如项目/cache/image_manager_disk_home_cache,启动时先初始化首页的磁盘缓存(现为10m)再初始化主体的磁盘缓存,避免日志文件过大导致的初始化时间太长阻塞App启动。

Other

感知生命周期

通过链式调用中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的时候释放图片缓存。但由于考虑到太激进的释放可能导致缓存复用效率

glide是怎么拿到的width,height(view未测绘前拿不到宽高)

如果view的布局宽高有值,或view本身的宽高有值,会直接返回布局宽高或view宽高。如果没有:

ViewTarget会为所持有的View注册view树绘制回调,待到经过测量布局之后回调onSizeReady()了,才会发起engine.load。(如果wrap_content则会用getMaxDisplayLength()屏幕长的一边作为宽高返回)

addOnPreDrawListener(OnPreDrawListener listener)

image-20240709154704558

如果是Wrap_content?

那么会返回 屏幕高(长的一边) * 屏幕高,比如 1080 * 1920 屏幕会返回 1920 * 1920兜底,在后续流程比如内存缓存Key生成时候,也是用的这个兜底宽高。

具体见ViewTarget.getMaxDisplayLength()

感知内存吃紧

注册ComponentCallbacks2,实现细粒度内存管理:

  1. onLowMemory(){清除内存}
  2. onTrimMemory(){修剪内存}
1
2
3
memoryCache.trimMemory(level); // 内存缓存
bitmapPool.trimMemory(level); // bitmap池
arrayPool.trimMemory(level); // 字节数组池

可以设置在onTrimMemory时,取消所有正在进行的请求。

Glide线程池

源线程池sourceExecutor

(触发下载时存取磁盘和解析使用的线程池,非下载线程池,下载包给okhttp/httpurlconnection了):

定长为 cpu数量(最大4)(核心线程和工作线程数量都最大为4)的线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private GlideExecutor getActiveSourceExecutor() {
return useUnlimitedSourceGeneratorPool
? sourceUnlimitedExecutor
: (useAnimationPool ? animationExecutor : sourceExecutor);
}

//bestThreadCount = Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors());

public static GlideExecutor newSourceExecutor() {
return newSourceExecutor(
calculateBestThreadCount(),
DEFAULT_SOURCE_EXECUTOR_NAME,
UncaughtThrowableStrategy.DEFAULT);
}

磁盘缓存diskCacheExecutor

(从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,则会命中gif解码器-将资源解码成GifDrawable,它持有GifFrameLoader会将资源解码成一张张Bitmap并且传递给DelayTarget的对象,该对象每次资源加载完毕都会通过handler发送延迟消息回调 onFrameReady() 以触发GifDrawable.invalidataSelf()重绘。加载下一帧时会重新构建DelayTarget

QA

ActiveResources弱引用问题

Q: glide 当前页面的图片缓存是存在activiteResource的弱引用的,也就是说当gc时当前页面的图片缓存会被回收?那岂不是当前页面会空白

A: 不会啊,因为实际的资源被页面的view持有着,而ActiveResources只是一层弱引用而已。而已经不被view持有(没显示出来的图片)但仍在活动缓存中的弱引用会被降级到LRU内存缓存中。

活动资源缓存的存在以较小的代价减小Lru缓存的压力,提升Lru缓存的效率。

原因是活动资源缓存通过缓存的对象本身就是在内存中进行使用,缓存是只是建立一个弱引用关系。如果过没有活动资源缓存,每一次使用的资源都加入内存缓存,极有可能因为放入Lru缓存的数据过多,导致正在使用资源从Lru缓存中移除,等到下次来进行加载的时候因为没有对应的引用关系,找不到原来内存中正在使用的那个资源,从而需要再次从文件或者网络进行数据加载。这样同一份资源需要使用两处或者多处内存。大大的提高了内存消耗。总而言之,活动资源缓存以较小的代价提高了Lru缓存的使用效率,防止加载中的资源被lru回收。

字节数组的复用池

BitmapPool

  • BitmatPool 是 Glide 维护了一个图片复用池,LruBitmapPool 使用 Lru 算法保留最近使用的尺寸的 Bitmap。
  • api19 后使用bitmap的字节数和config作为key,而之前使用宽高和congif,所以19以后复用度更高
  • 用类似LinkedHashMap存储,键值对中的值是一组Bitmap,相同字节数的Bitmap 存在一个List中(这样设计的目的是,将Lru策略运用在Bitmap大小上,而不是单个Bitmap上),控制BitmapPool大小通过删除数据组中最后一个Bitmap。
  • BitmapPool 大部分用于Bitmap变换和gif加载时

ArrayPool

  • 是一个采用Lru策略的数组池,用于解码时候的字节数组的复用。
  • 清理内存意味着清理MemoryCache,BitmapPool,ArrayPool

相关源码

load可加载多种model的图片

image-20240815203503827

1
2
3
4
5
6
7
8
9
10
11
RequestBuilder.java
public RequestBuilder<TranscodeType> load(@Nullable Object model) {
return loadGeneric(model);
}

@NonNull
private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
this.model = model;
isModelSet = true;
return this;
}

获得View宽高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SingleRequest.java
public void onSizeReady(int width, int height) {
//...
//加载图片的View在viewTreeObserver的onPreDrawListener回调回来后拿到确定的View宽高
loadStatus =
engine.load(
glideContext,
model,
requestOptions.getSignature(),
this.width,
this.height,
requestOptions.getResourceClass(),
transcodeClass,
priority,
requestOptions.getDiskCacheStrategy(),
requestOptions.getTransformations(),
requestOptions.isTransformationRequired(),
requestOptions.isScaleOnlyOrNoTransform(),
requestOptions.getOptions(),
requestOptions.isMemoryCacheable(),
requestOptions.getUseUnlimitedSourceGeneratorsPool(),
requestOptions.getUseAnimationPool(),
requestOptions.getOnlyRetrieveFromCache(),
this,
callbackExecutor);
//...
}

内存缓存Key的生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
Engine.java
public <R> LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class<?> resourceClass,
Class<R> transcodeClass,
Priority priority,
DiskCacheStrategy diskCacheStrategy,
Map<Class<?>, Transformation<?>> transformations,
boolean isTransformationRequired,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb,
Executor callbackExecutor) {

//内存缓存Key的生成:由model(所加载的数据的模型:Uri/String/Bitmap等)、singature(请求SingleRequest的签名、ImageView的宽高、请求变换(Transformations)、resourceClass(资源解码类型,asBitmap是Bitmap.class,asGif是GifDrawable.class,没有指定是Drawable.class),transcodeClass(链式中调用.transcode()传入,一般为Drawable.class,是最后ImageView实际),options(一组存储如压缩质量、压缩格式、网络超时时间等选项的类)
//这八者任意一个改变都会引起key的hashcode变化
EngineKey key =
keyFactory.buildKey(
model,
signature,
width,
height,
transformations,
resourceClass,
transcodeClass,
options);


EngineResource<?> memoryResource;
synchronized (this) {
//是否存在内存缓存(包括 activiteResource 和 cache)
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

//没有内存缓存,就进行网络或者磁盘缓存解析
if (memoryResource == null) {
return waitForExistingOrStartNewJob(
glideContext,
model,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
options,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache,
cb,
callbackExecutor,
key,
startTime);
}
}

//只有命中内存缓存才从这里返回
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
return null;
}

请求缓存流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Engine.java {
@Nullable
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}

//该缓存key存在于activiteResource,是弱引用存在于内存中的
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}

//该缓存存在于cache
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}


//如果该缓存key存在于activiteResource,对应的对象引用加1
@Nullable
private EngineResource<?> loadFromActiveResources(Key key) {
EngineResource<?> active = activeResources.get(key);
if (active != null) {
active.acquire();
}

return active;
}

//如果该缓存存在于cache,将其中cache中移除并存到activiteResource中并引用计数加1
private EngineResource<?> loadFromCache(Key key) {
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.activate(key, cached);
}
return cached;
}

private EngineResource<?> getEngineResourceFromCache(Key key) {
Resource<?> cached = cache.remove(key);

final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
//一般情况都是从缓存cache中直接返回
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource<?>) cached;
} else {
//兜底
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
}

相关概念

  1. Resource

Resource 代表了解码后的资源,即已经从原始数据(如网络图片的字节流,一般为AVIF、WEBP)解码并转换为可直接使用的数据形式(如 Bitmap、Drawable 等,如果存到磁盘会encode成jpeg/png格式)。Resource 封装了解码后的资源,并提供了一些管理功能,如引用计数和资源回收。

  1. DataCache

DataCache 是指数据缓存,主要用于缓存原始数据(例如,通过网络请求获取的图片字节流)。数据缓存可以加速后续的解码过程,因为原始数据已经被缓存下来,不需要再次从网络或其他源获取。

  1. Source

Source 代表数据的源头,即原始数据的来源。常见的源包括网络(主要)、文件系统、资源文件、内容提供者等。
网络源:从 URL 或网络地址加载数据。
文件源:从本地文件系统加载数据。
资源源:从应用的资源文件中加载数据。
内容提供者:通过内容提供者(ContentProvider)加载数据。

  • URL:主要用于标识网络上的资源。示例:https://example.com/image.jpg
  • URI:是一个更广泛的概念,可以标识任意一种资源,包括本地文件和网络资源。示例:file:///storage/emulated/0/Download/image.jpg 和 content://media/external/images/media/12345

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 或其他自定义类型。

相关参考https://juejin.cn/post/7129306281650683935#heading-92

empty

  • PackageManagerService

  • WindowManagerService(done?)

  • ActivityManagerService(done?)

  • ServiceManager(done?)

  • Binder
  • ANR

1、review

kernel bright spots

  • Apm监控
  • fresco深入优化
  • 性能优化工作/工具

skills pool

  • 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四级缓存

2、TODO

  • kotlin协程
  • 算法(1~2)
  • View测绘,事件分发(事件系列只能被整一个处理)
  • SurfaceView, TextureView及View的区别。SurfaceView怎么控制它处于的层级
  • SMTP了解下

3、Optimize Experience

  • 图片内存优化工作:fresco关于gif缓存问题修复,dokit图片闪烁缓存失效问题的修复、基于dokit框架(ASM)下尺寸过大图片识别实现。(基于业务做的一些图片加载的优化,如cdn链接统一域名,regex(String.replace)耗时200us)

  • 耗时方法,两种常见的监听方式:looper.printer 以及 编舞者回调监听

  • recyclerview优化

4、Other

  • h5图片缓存共用的优化思路
  • 阿里patron和Metrix做的native hook 减少32bit模式下app内存占用

Project Experience

从购物车重构到购物车预加载

ps:我并不想抽象的去将购物车的架构之类的东西,而是更具体的讲遇到的问题及解决方案。不过还是得简单讲一下设计

购物车代码十分屎山,难以维护。在23年时做了一次历经一个月的重构,边开车边换轮子。使用多数业务在用的协议架构奥创重构:先说收益:奥创加购以组件为单位,职责清晰,易于复用维护,端侧组件使用MVVM架构进一步解耦逻辑:

举个例子,请求在model层(共用的Repository,维护于Engine层),每个组件通过注册组件的ViewHolder获得独立的View,注册组件的Parser获得独立的ViewModel解析数据及进行业务逻辑处理。view与vm之间使用liveData进行数据更新后的通知view状态渲染。

遇到的问题:

  1. 奥创冗余数据及后端接口慢 -> 购物车本地数据缓存 + (detail/购物车进下单页)的数据预加载 + 购物车预加载的根据abtest下做的精细运营。
  2. LiveData的使用:购物车提供给MainActiivty的其他Tab由于时机问题导致的刷新问题
  3. 多国家配置不同组件 -> 问题:一些组件不同国家需要配置不同的样式和逻辑,但是下发的组件名是一致的。那么当时有两种做法:一种是直接在解析是篡改掉网络数据中的组件名,本地正常映射。但是这个评估完认为直接修改了数据,风险更不好控。于是我在注册组件名时根据依靠 同名Parser后注册的Parser优先匹配的机制 实现 的特性,在注册Parser时提供将多国家的parser后置接口,于是当解析完多国家组件parser后即不会再映射通用组件了,即不影响原数据,也实现了多国家不新增后端组件。

RTL adapte

  1. What: Ltr下不同币种导致的显示顺序错乱;
  2. How: 服务端配置下发特殊符号
  3. More:
  1. 做过印象比较深的事情 ,有挑战的事情

  2. 弱网环境优化:业务和技术的手段,有什么很技术的手段来做吗

  3. 当前团队的优劣点

  4. 遇到不合理需求你会怎么拒绝

  5. 有用过新的东西吗

  6. 线上问题怎么解决,因为不能用动态字节码技术

  7. 遇到啥有意思的问题吗,crash。window

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    java.lang.IllegalArgumentException: View=android.widget.PopupWindow$PopupDecorView{2a10009 V.E...... R.....I. 0,0-0,0} not attached to window manager
    at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:544)
    at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:433)
    at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:162)
    at android.widget.PopupWindow.update(PopupWindow.java:2226)
    at android.widget.PopupWindow.update(PopupWindow.java:2347)
    at android.widget.PopupWindow.alignToAnchor(PopupWindow.java:2517)
    at android.widget.PopupWindow.-$$Nest$malignToAnchor(Unknown Source:0)
    at android.widget.PopupWindow$1.onViewAttachedToWindow(PopupWindow.java:243)
    at android.view.View.dispatchAttachedToWindow(View.java:21423)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3502)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3509)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3011)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2518)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9389)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1451)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1459)
    at android.view.Choreographer.doCallbacks(Choreographer.java:1089)
    at android.view.Choreographer.doFrame(Choreographer.java:1003)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1431)
    at android.os.Handler.handleCallback(Handler.java:942)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:210)
    at android.os.Looper.loop(Looper.java:299)
    at android.app.ActivityThread.main(ActivityThread.java:8261)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:559)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:954)

OOM的治理:crash率在一次架构组的升级后由万4涨到万6,其中新增了很多的OOM。1:修复了EventCenter的内存泄露;2:搜推服务造成的泄露;

内存兜底措施:2g设备直接改为RGB565,其他才开ARGB8888;同时降低起设备图片内存池大小;

Kotlin

kotlin data class

data class会自动生成以下方法:

  • equals()

  • hashCode()

    1
    2
    3
    4
    5
    public int hashCode() {
    int var10000 = Integer.hashCode(this.age) * 31;
    String var10001 = this.name;
    return var10000 + (var10001 != null ? var10001.hashCode() : 0);
    }
  • toString()

  • copy()

  • componentN()

    编译器会为数据类生成 组件函数(Component function), 有了这些组件函数, 就可以在 解构声明(destructuring declaration) 中使用数据类:

    1
    2
    3
    4
    val jane = User("Jane", 35)
    val (name, age) = jane
    println("$name, $age years of age")
    // 输出结果为 Jane, 35 years of age
  • 属性的get()/set()

    val的属性不会有setter

  • constructor()

    只有有参构造函数,没有无参构造函数。fastJson解析会抛该异常,需升级到高版本并引入kotlin-reflect依赖

Read more

Surface

  • SurfaceView: View 的子类,但不与宿主 Window 共享 Surface, 而是有自己独立的 Surface, 且可以在一个独立的线程中进行绘制,因此 SurfaceView 一般用来实现比较复杂的图像或动画/视频的显示。可以参考 Android双缓存与SurfaceView。由于其内容是绘制在一个独立的 Surface 上,因此无法用 scrollTo/By 等方法去移动操作 Canvas 里的内容,但是可对整个 View 进行平移,缩放,旋转等变换操作。
  • GLSurfaceView: 基于 SurfaceView 再次进行扩展,在 SurfaceView 基础上封装了 EGL 环境管理以及 Render 线程,专门为 OpenGl 显示渲染使用。参考 Android-OpenGL-ES笔记
  • TextrueView: Android 4.0 后引入 TextureView, 它将 SurfaceTexture 和 View 结合到了一起。与 SurfaceView 相比,它并没有创建一个单独的 Surface 来绘制,解决了 SurfaceView 无法在 Canvas 内容上做动画的问题。另外 TextureView 必须在硬件加速开启的窗口中使用。
Read more

SystemStartProcess

image-20210730142407367

简述:

开机后系统将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)、WMSPKMS、PMS等服务。

App:由Zygote孵化的第一个App——Launcher(桌面),用户点击Launcher上的app图片,通过JNI调用AMS从Zygote进程中fork出新的App。

Read more

matrix

Matrix Tencent

一:TraceCanary

插桩:通过插桩,在除了get/set、默认或匿名构造函数等简单函数外的所有方法,入口/出口插入MethodBeat.i()/MethodBeat.o()。

image-20210729175300794

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//AppMethodBeat.java
private static void mergeData(int methodId, int index, boolean isIn) {
if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
}
long trueId = 0L;
if (isIn) {
trueId |= 1L << 63;
}
trueId |= (long) methodId << 43;
trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
sBuffer[index] = trueId;
checkPileup(index);
sLastIndex = index;
}

用一个64位的long型来存储方法的:方法开始/方法结束(最高位63位)、方法id(递增,43到62位)、当前与MethodBeat模块初始化时差(0到42位)

Read more

EventDispatch

image-20220127160157749

Core

事件分发中有一个重要的规则:一个触控点的一个事件序列只能给一个view处理

分析:以DOWN事件为序列分发判定,ViewGroup为消费DOWN事件的View生成一个TouchTarget(这个TouchTarget就包含了该view的实例与触控id,id可以是多个以应对多指触控),后续MOVE、UP都会交给这个TouchTarget。如果TouchTarget为空则ViewGroup自己处理。如果viewGroup消费了down事件,那么子view将无法收到任何事件。

Read more

插件化/热修复

image-20210410164426213

插件化和热修复不是同一个概念,虽然站在技术实现的角度来说,他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;但是二者的出发点是不同的。插件化顾名思义,更多是想把需要实现的模块或功能当做一个独立的提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。热修复则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。

  • PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,也就是我们一开始提到的补丁。
  • BaseDexClassLoader: 是 PathClassLoader 和 DexClassLoader 的父类,其内有一个 DexPathList 属性,实现了 findClass 方法逻辑,PathClassLoader 和 DexClassLoader 都只是在构造函数上对其做了简单封装而已。
Read more

AndroidVm

热知识:java常见的虚拟机如Hotspot虚拟机是基于栈结构的,而Dalvik是基于寄存器结构的。

常见的java虚拟机跑的是.class文件,而Dalvik跑的是.dex(.odex)文件。

image-20210602170532688

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
2
3
4
5
if (Build.VERSION.SDK_INT <= 19) { 
BoostMultiDex.install(this);
} else {
MultiDex.install(this);
}
Read more

JsBridage

Js与原生交互的方式:

通过注入对象或拦截 URL SCHEME实现JSBridge

Read more

fresco

Fresco(2.5.0)

以MVC的 Fresco架构入手,层层递进分析fresco的整体思路。

img

Read more

retrofit

原理-动态代理

*Retrofit通过 反射构建一个 接口的 实现类(动态代理本质就是反射),其中每个重写方法被调用时,都会回调到InvocationHandler.invoke中,invoke回调时(只要是不是object类中的方法)都会通过 获取到的方法的注解、方法的名称、方法的返回值、方法参数的注解、方法参数类型等等所需信息, 解析成一个ServiceMethod对象(放入缓存池),ServiceMethod根据获取到的方法信息,构建OkHttp请求,并将结果通过converter转换后回调给最初传入的Callback*

Read more

okhttp

链接: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))执行它调度,执行的过程也就是asyncCall的run()方法通过责任链五大拦截器进行层层处理的过程。

Read more

leakcanary

  1. 线程栈中的局部变量表引用的所有变量,即运行线程中引用到的所有变量,包括线程中方法参数和局部变量

  2. 存活的线程对象

  3. native 的 jni引用

  4. class 对象 (classLoader 不会卸载class)

  5. 引用类型的静态变量

// 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中剩余的即为可能泄露的对象

Read more

Image

内存所在位置

Android 2.3 之前: 像素数据存在于native heap

Android 3.0 ~ 7.1 之间: 存在于 java heap

Android 8.0及之后: 存在于 native

Read more

Android Build

Apk总构建流程

简述

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

img

Read more

ScreenDraw

从onCreate到页面绘制

当手指点击了桌面的App图标时发生了什么 - ProcessOn

Android 屏幕刷新机制

主要参考 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的测量布局绘制

Read more