网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
难道导致泄漏的这个 `BasicInflater` 是其他地方创建出来的? 首先反射创建不大可能,因为这个类没有keep,那么最有可能的就是下面这个路径了:
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new BasicInflater(newContext);
}
而这个方法似乎只在ViewStub中使用:
// layoutinflater
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// …
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
// …
}
这个clone出来的 inflater 会被 ViewStub.mInflater 持有,但是从内存数据来看,**它自己是gc root,并且没有其他对象持有它**:

而这个 BasicInflater 之所以是 gc root,是因为它是在当前Java frame的本地变量表中,再回头看一下相关代码:
public void runInner() {
InflateRequest request;
try {
request = mQueue.take();
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
return;
}
try {
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException ex) {
// Probably a Looper failure, retry on the UI thread
Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
+ " thread", ex);
}
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}
@Override
public void run() {
while (true) {
runInner();
}
}
###### 第二次猜想
倘若 `runInner()`被内联到 `run()`中,那么正好就是这个情况,并且能解释上面的所有现象,于是找了个**线下包**看了下,结果惊呆了:没有内联。。。
无奈,似乎没有思路了,但是也不能不处理啊,翻了下业务代码后发现有个`LiveAsyncLayoutInflater` 它就是从 `AsyncLayoutInflater`中copy出来的,唯一的改动就是在 `runInner()` 中,取出 request 执行 inflate之前先判断一下 context(activity)是否已经destroy,如果是的话,直接丢弃这个 request。跟这个代码的作者聊了下,他这样做也是因为担心Activity销毁后还有没处理完的request。
###### 第一次尝试修复
虽然我们上面分析过导致这个内存泄漏的原因似乎不是 因为“Activity销毁后还有没处理完的request”,但是我搜了一下发现却真没有跟 `LiveAsyncLayoutInflater` 相关的泄漏,业务用法也都一样。。。 没有其他思路,于是也这样改一下试试吧,起码不会有坏处。并且我们还加了一个逻辑: 监听Activity destroy,然后遍历 request 队列,把和其关联的 request 移除。这样的目的是因为有些 request 是用的 Application context 来处理的,只在 `runInner` 中判断,可能会影响后面 request 处理的及时性。
尝试修复上线后发现跟预期的基本“一致”,可以说是毫无作用 😂😂😂
#### 第三次分析
尝试一下看看线下能不能复现,看看复现的场景是什么。
目前我们线下包在 activity destroy的时候都会去分析泄漏,于是改了下代码,当发现跟当前问题引用链一样的时候就将额外补充的一些信息一起上报上来排查。
然而用这个包测试了一段时间,没复现。。。然后又看了下线下的内存泄漏监控,这个每天也会上报不少的泄漏问题,结果这个问题一个都没有。。。
线上量很大,线下一个都没有,难道是某处逻辑线上包跟线下包不一样触发了这个问题?
回想一开始分析的时候有个判断:如果`runInner`被内联到`run`里面,那问题就可以解释,当时反编译已经排除了,但是忽然想到当时包用错了。。。拿的是线下包,线下包都是 fast 模式,是不会走 optimized 的,所以肯定不会内联。相关配置如下:
if (BuildContext.isFast) {
// …
proguardFile ‘proguard_not_opt.pro’
// …
}
// … proguard_not_opt.pro
-dontoptimize
// …
因为 optimize 特别耗时,线下包关闭也是很合理的。但是线上包是没有这个配置的,也就是打开了 optimize。另外混淆配置中没有关闭 unique method inline:
下面的没有配:
-optimizations !method/inlining/unique
而 `runInner` 也只有一处调用,在打开optimize且没有禁止 unique method inline 时是可能inline的。于是找了个线上包再来看看,果然 **内 联 了 !!!**
既然内联了,那么 runInner 的本地变量表中的对象就被合到了 run 中,而 run 里面是个 while (true) 死循环,生命周期无限长,所以如果这里面的本地变量表中持有 BasicInflater 那么它就是gc root,并且进一步导致它的 context 泄漏,我们来看下AsyncLayoutInflater的这段代码:
.method public final run()V
.registers 6
.prologue
:catch_0
:goto_0
:try_start_0
iget-object v0, p0, LX/pJX;->LIZIZ:Ljava/util/concurrent/ArrayBlockingQueue;
invoke-virtual {v0}, Ljava/util/concurrent/ArrayBlockingQueue;->take()Ljava/lang/Object;
move-result-object v4
check-cast v4, LX/pJZ;
const/4 v3, 0x0
:try_end_9
.catch Ljava/lang/InterruptedException; {:try_start_0 … :try_end_9} :catch_0
:try_start_9
iget-object v0, v4, LX/pJZ;->LIZ:LX/pJW;
iget-object v2, v0, LX/pJW;->LIZ:Landroid/view/LayoutInflater;
iget v1, v4, LX/pJZ;->LIZJ:I
iget-object v0, v4, LX/pJZ;->LIZIZ:Landroid/view/ViewGroup;
invoke-virtual {v2, v1, v0, v3}, Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View;
move-result-object v0
iput-object v0, v4, LX/pJZ;->LIZLLL:Landroid/view/View;
:try_end_17
.catch Ljava/lang/RuntimeException; {:try_start_9 … :try_end_17} :catch_17
:catch_17
iget-object v0, v4, LX/pJZ;->LIZ:LX/pJW;
iget-object v0, v0, LX/pJW;->LIZIZ:Landroid/os/Handler;
invoke-static {v0, v3, v4}, Landroid/os/Message;->obtain(Landroid/os/Handler;ILjava/lang/Object;)Landroid/os/Message;
move-result-object v0
invoke-virtual {v0}, Landroid/os/Message;->sendToTarget()V
goto :goto_0
.end method
1. 从 `iget-object v2, v0, LX/pJW;->LIZ:Landroid/view/LayoutInflater;` 知道 BasicInflater 被赋值到了 `v2`寄存器
2. `invoke-virtual {v2, v1, v0, v3}, Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View;` 通过 `v2`中的`BasicInflater`去inflate 布局
3. **`v2`寄存器没有复用,当通过 `goto :goto_0` 进行下一次循环,并且取出下一个 request,`v2` 被赋予下一个 `BasicInflater` 实例引用之前,`v2` 一直持有者上一个 `BasicInflater` 引用,而这个就是导致泄漏的引用。** 当前处于while循环中,等待下一个request,跟我们上面分析的“导致泄漏的BasicInflater处于空闲状态一致”,并且长时间处于这个状态,所以抓到的概率就很大了。
为什么`AsyncLayoutInflater$BasicInflater 的实例数始终比 AsyncLayoutInflater多一个呢?`,原因是:AsyncLayoutInflater 的引用是存在 `v0` 寄存器中的,而`v0`寄存器被多次复用,所以AsyncLayoutInflater的引用并没有被一直持有。
到此问题基本就分析清楚了,但是还有一个遗留问题,上面提到的业务中copy出来的 `LiveAsyncLayoutInflater`为什么没有泄漏呢?原因是:
1. 他的 `BasicInflater` 引用也是放到 `v2`寄存器中的
2. 但是这个类中的方法有插桩,`runInner`被内联之后,插桩代码也被内联了,在进行while(true)的下一轮循环时,首先会去执行插桩代码,而插桩代码复用了`v2`寄存器,因此就不再持有`BasicInflater`的引用了,因此没有泄漏
#### 修复
问题已经明确,修复就比较简单了,只要让 `runInner` 不内联就行了。而之所以它会被内联,是因为 `proguard/R8` 有个优化,如果某个方法只有一处调用(当然还要满足很多其他条件),那么就将它内联,并删除原方法。因此我们改一下,找一个其他地方调一下就可以规避,比如:
if (/* 此处返回 false,让if block不执行就行 */) {
runInner();// Make sure never reach here
}
这样静态分析不是一处调用,不会内联,实际上也不会走到,也不影响逻辑。
不过线上我没有这样改,因为还有其他办法可以不用改代码:给 `runInner` keep 一下就也不会内联了,原因也好理解:如果方法被keep了,那么原方法不能删,而这个又不是个小方法,要是内联的话,字节码变大了,方法数也没少,必然负向了,那还内联干啥。
-keepclassmembers class androidx.asynclayoutinflater.view.AsyncLayoutInflater$InflateThread {
public void runInner();
}
改了之后,泄漏解决了~😊
#### 顺便提一句
在 `runInner` 里面判断context(activity)是否destroy,如果destroy的话,就拦截不处理这个 request还有个小麻烦:`onInflateFinished` 这个回调是否要触发?
public interface OnInflateFinishedListener {
void onInflateFinished(@NonNull View view, @LayoutRes int resid,
@Nullable ViewGroup parent);
}
1. 如果要回调`onInflateFinished`,那么 view 如何获取?null 肯定不行,因为原本接口中有 `@NonNull`,导致很多业务代码不会判空
2. 不回调也不行,因为有些业务有个“优化”逻辑,如果上一个 inflate 没有回来,后续就走同步 inflate,所以如果不回调,相当于关闭了异步 inflate 功能。。。
所以当时我们加了个 `onCancel` 回调,业务可以在这里处理被拦截的情况:
public interface OnInflateFinishedListener {
void onInflateFinished(@NonNull View view, @LayoutRes int resid,
@Nullable ViewGroup parent);
/**
* if context (activity) destroyed, InflateRequest will be cancel, and this method will be invoked.
*
* It can be invoked on different thread
* @param resid
*/
default void onCancel(@LayoutRes int resid) {}
}
比如:
override fun onCancel(resid: Int) {
isAsyncInflating = false
}
但是这样也不优雅,而且也不是所有业务都知道有这么个 `onCancel` api,如果改成非default接口,又要改动很多地方的代码。
好在上面也看到了这个泄漏并非因为 “Activity destroy后还有没处理完的 InflateRequest,可能导致短暂泄漏”,拦截的必要性也不大。
#### Android 学习笔录
**Android 性能优化篇:[`https://qr18.cn/FVlo89`]( )**
**Android 车载篇:[`https://qr18.cn/F05ZCM`]( )**


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618636735)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
-9gxTzlVJ-1715719412034)]
[外链图片转存中...(img-daGfz4BL-1715719412034)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618636735)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**