记一次inline使用不当导致编译期Null指针的排查过程

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 周五的一个下午,我哼着小曲和往常一样合完代码。准备运行试试看,结果build时发现了这样一个异常。

起因

周五的一个下午,我哼着小曲和往常一样合完代码。准备运行试试看,结果build时发现了这样一个异常。

InlineParameterChecker NullPointerException

一般对于这种编译期间的异常,原因往往并不是很容易能快速定位,因为往往都是业务代码出现的问题,如果某次合并更改很多,比如我这一次,重构了底层的某个组件,所以直接当场裂开😑。于是接下来整个任务都变成了如何找到 错误的 代码处。

先说结论

当方法添加了 inline 修饰后,即也就是内联之后,如果方法参数是一个函数对象(lambda),那么不可为 null

一般情况下 IDE 会主动提示你,如下所示:

但是特殊情况下,如下错误示例:

某一天,程序员小P 突然发现一段代码,善用Kotlin的他,觉得这里可以使用 inline 可以优化,于是下意识就加了一个 inline ,但是没有仔细看方法参数,反正IDE没报错,即也就是自己还没意识这个更改带来的严重性。

但是一旦改完之后,没有 build ,那么这就是一个隐藏的坑,严重一点可能会导致你好几个小时找不到原因。

如何定位错误代码

如果直接对着代码找,那么可能就需要对比所有相关 inline 相关的代码,如果使用之处不多,那么也能很快定位。

但是最关键的问题在于: 我怎么知道那段代码有问题呢,虽然我知道是inline的问题,但是具体是什么呢,我现在不知道啊😢,所以这种方法暂时也只能放弃。

google三连

身为一个开发经验小成的老鸟,这肯定难不倒我,直接google三连

网络异常,图片无法展示
|

什么玩意都是。

难道不应该直接搜索如何打印完整的 build 日志吗,然后通过日志查看到底在哪一步失败了,于是刚好想起了前几天同学也发现过这样的问题,直接去问他。

网络异常,图片无法展示
|

打印build详细日志

波澜不惊,这样就可以了,嘿嘿嘿,好简单😅。

于是,改完,开始build…等待 loading

内存不足,那就再改 gradle 内存,这下应该可以了吧,重启as,开始build…等待loading😑:

这…,还是看不到具体日志啊,难道是真的看不了吗,花了10几分钟就这😤?

gradlew assembleDebug

继续尝试别的方案,又是一顿搜索,这时候看到了 StackOverFlow 上有人用这个命令也可以,于是死马当活马医,继续尝试。

执行 ./gradlew clean assembleDebug 开始尝试。结果如下:

我裂开了,于是继续找其他方案,来来回回折腾了快1个小时,还是这样,难不成我只能去对代码了吗?

太痛苦了,这时候只能寻求坐在我对面的开发组大佬帮助,希望能解决问题,阿门🙏。

让大佬来看了一下,大佬的回复很简单:

这应该已经是gradle能给出的最大提示了,你想要的错误具体位置,应该是无法打印出的,这种情况,你只能通过合并的diff对比下,看看是哪里导致的。

是啊,我忙活了这么久,居然忘记了最简单的,我直接去看git合并的diff啊,因为这个是 inline 的报错,那么范围也就只有 改动过的inline 啊,瞬间感觉自己走错方向了。

通过git-合并diff对比

直接 github 找到 pr ,点击 Files changed , command+F 搜索 inline ,瞬间流下来开心的泪水🤧.

果然范围小多了,那么接下来只需要找 inline 具体更改位置。

于是乎,就发现上述代码,似乎不太对劲,乍一眼看上去没啥,但整个 inline 相关的更改里,只有这段新增了一个 inline 修饰。

于是试着去掉 inline ,发现居然好了…

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KuVIHXMn-1629016681853)(https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRoaWgoOY997ra2DongBtQ6Xzqzql-qgEnlpw&usqp=CAU)]

反思

到了这里,我似乎不敢相信自己,在之前的开发中,我从不知道 inline 修饰的方法,参数是函数对象时不可为 null ,于是我赶紧去搜了下相关文章:

google

网络异常,图片无法展示
|

度娘

网络异常,图片无法展示
|

结果并没有发现相关有价值的文章,可能很少人像我那样操作,的确一般情况下,IDE 都会准确提示错误信息。

对比转换后的java代码,结果也是报错,也没有什么可奇怪的。

于是接连测试了下:

结果也很简单。对于 inline 修饰的方法而言,如果方法参数是基本对象,那么可以为 null ,如果是 函数对象(lambda) ,则不可为 **null ** ,否则将引发编译错误。

那到底为什么呢?

难道网上没有资料,这个问题就要烂在这里了吗,我不太甘心,既然没有现成,那我们就从 inline 的本质出发,寻找原因:

我们都知道,inline 的本质是在编译器将相关代码直接拷贝到了调用的地方,也就是说,比如我们上述截图中的 testObj  方法中的 obj 函数对象,其具体实现,会最终变成如下例子。

假设如下:

public final void testObj2(@Nullable Function0 obj) {
   int $i$f$testObj2 = 0;
}

里面的Function是具体可量化的接口,其是Kotlin提前定义好的。

但是现在,obj函数对象 可能为  null,即编译器没法确定了,编译器不知道这里到底应该复制什么玩意,如果不复制,那还怎么优化,但怎么复制,你都是 null 的,我怎么知道呢,所以直接 null 指针了。

当然上述过程我是猜测的,因为我没办法知道其内部原理,但是我相信实际和我推测的应该差别也不是很大。

碎碎谈、

这虽然只是一个开发中不怎么常见的问题,但从整个过程而言,我的做法或许并不是很好,如果我一开始选择最直接的方式,效率会更高点,但也许我也会失去对一些东西的探索,之所以写本篇,很大程度上也是因为我在这个问题上所花费的时间让我必须写下来记录一下,希望这个过程可以帮助到大家。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
C中的NULL指针
【2月更文挑战第6天】C中的 NULL指针。
55 2
C 中的 NULL 指针
C 中的 NULL 指针。
35 0
|
6月前
C中的NULL指针
C中的NULL指针。
42 2
|
6月前
C中的NULL指针
C中的NULL指针。
23 1
|
6月前
|
编译器 C语言
【C语言】深入理解NULL指针
【C语言】深入理解NULL指针
153 0
|
程序员 C语言 C++
C++空指针NULL和nullptr
🐰C++空指针NULL和nullptr 🏡空指针
|
SQL Java 关系型数据库
记录:springboot使用resultType空指针java.lang.NullPointerException: null...【亲测有效】
记录:springboot使用resultType空指针java.lang.NullPointerException: null...【亲测有效】
423 0
|
C语言 开发者
void 指针和 NULL 指针|学习笔记
快速学习 void 指针和 NULL 指针
|
C语言
void指针和NULL指针
一、void指针 二、NULL指针 三、NULL不是NUL
|
C语言 C++
【C 语言】指针数据类型 ( 不允许向 NULL 地址写入数据 | 不允许不断地改变指针指向 | 字面量存放位置 )
【C 语言】指针数据类型 ( 不允许向 NULL 地址写入数据 | 不允许不断地改变指针指向 | 字面量存放位置 )
226 0
【C 语言】指针数据类型 ( 不允许向 NULL 地址写入数据 | 不允许不断地改变指针指向 | 字面量存放位置 )