2024年HarmonyOS鸿蒙最新编译内联导致内存泄漏的问题定位&修复(4),2024年最新被面试官怼了还有戏吗

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!


img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以戳这里获取

// 子线程需要把 mQueue 里面的 request 处理完,而request 持有 inflater,inflater 持有 context

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();
}
}


根据上面的分析,这种泄漏理论上是存在的,但是 inflate 一个layout一般很快,几毫秒、几十毫秒、最多几百毫秒,这种属于短时泄漏,而且时间特别短,影响不大,所以第一次简单看了下这个问题后觉得影响不大不必处理😂


#### 第二次分析


虽然上面判断这个“影响不大,且泄漏时间很短”,但是每个版本都会触发报警,而且是量级很大的泄漏,于是继续排查一下这个泄漏是否有其他原因~


从内存泄漏的量级看,之前的判断似乎有点说不通:


1. 泄漏时间窗口这么短,hprof 刚好就在这期间dump的概率极低,量怎会这么大?
2. 短时泄漏问题其实很多,比如业务 postDelay 一个几秒的Runnable(持有外部类引用),在这期间 activity destroy了,也会出现短时泄漏,这个时间几秒,比 inflate 要长多了,而且业务上这类情况很多,但是线上抓到的这类情况很少。


因此第一次分析的情况似乎不太对,于是捞个 hprof 再分析一下看看:


![](https://img-blog.csdnimg.cn/img_convert/916b86ca29cdefd3e56e4f51b3406e3b.webp?x-oss-process=image/format,png)


我们先通过内存信息来验证下我们第一次分析的猜测原因(activity destroy的时候,异步 inflate 任务还没执行完成)是否正确:


1. 看下 InflateThread 的 mQueue 中还有多少 InflateRequest 待处理:


![](https://img-blog.csdnimg.cn/img_convert/8661e07a30107e17532fe74ac77bbc69.webp?x-oss-process=image/format,png)


捞了很多个hprof,结果都是如此令人惊讶:**InflateRequest 队列都是空的,里面没有任务待处理!!!**


2. 再一次猜想:会不会是每次dump hprof的时候,刚好最后一个 InflateRequest 被从队列中取出来了,但是还没有执行完成呢?其实想想这种概率已经不能再低了,但是目前也没有其他怀疑点,换个角度看,假设是这种情况,会不会是某个布局有点问题,导致 inflate 耗时特别长,然后增加了被抓到的概率呢?


先来看下那个持有activity的 BasicInflater 信息:


![](https://img-blog.csdnimg.cn/img_convert/924599769cfd27d397a3281606ac27db.webp?x-oss-process=image/format,png)


我们知道LayoutInflater在inflate开始前会把当前要用的context存到他的 mConstructorArgs[0] 中,inflate 完成后再把 mConstructorArgs[0] 恢复,可以参考如下代码:



public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// …

    Context lastContext = (Context) mConstructorArgs[0];  
    mConstructorArgs[0] = inflaterContext;  

	try {
	
     // ...... do inflate
     
    } finally {  
        // Don't retain static reference on context.  
        mConstructorArgs[0] = lastContext;  
        mConstructorArgs[1] = null;  

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);  
    }  

    return result;  
}  

}


也就是说如果正在 inflate,mConstructorArgs[0]应该持有context,但是我们看到这个时候 BasicInflater 中的 mConstructorArgs 的2个element都是 null,也就是说**当前它处于空闲状态,并非正在 inflate!!!**


###### 初步结论


根据目前的信息来看,**泄漏的Activity是被一个空闲的 BasicInflater 持有的。**


进一步排查,发现一个更奇怪的现象:`在dump出来的 hprof中,AsyncLayoutInflater$BasicInflater 的实例数始终比 AsyncLayoutInflater 刚好多一个,而多出来的那一个就是导致 activity 泄漏的那个实例`


![](https://img-blog.csdnimg.cn/img_convert/9bcfbb6c5142ebd78c70dcaaf1837eaf.webp?x-oss-process=image/format,png)


从代码上看,`AsyncLayoutInflater$BasicInflater` 都是在 `AsyncLayoutInflater`的构造函数中创建的,按理说`AsyncLayoutInflater$BasicInflater`不会比`AsyncLayoutInflater`更多才对🤔️



public AsyncLayoutInflater(@NonNull Context context) {
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,并且没有其他对象持有它**:


![](https://img-blog.csdnimg.cn/img_convert/f7881013eae30c29e284367dae823846.webp?x-oss-process=image/format,png)


而这个 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

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

mg-9I0piIjG-1715638453358)]
[外链图片转存中…(img-BMSdngBr-1715638453358)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 38
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值