一、背景
最近应用商店开始开始逐步清理安卓SDK版本低于30的软件,为了保证应用可以继续上架任务商店,我们升级了应用的sdk,在解决了一系列升级导致的空异常后,项目摇摇晃晃的 跑了起来,然而来不及高兴,应用就崩溃了,排查后定位到了项目中的工具类——ToastUtil,这个工具类由于空异常导致了项目的崩溃,接下来就是熟悉的改bug环节了。
二、问题分析
先来看下工具类的关键代码:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
mToast = Toast.makeText(mContext, null, Toast.LENGTH_SHORT)
mToast?.setText(message)
} else {
if (mToast == null) {
mToast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT)
mToast?.setText(message)
} else {
mToast?.setText(message)
}
}
val textview_id = Resources.getSystem().getIdentifier("message", "id", "android")
val textView = mToast?.view?.findViewById<View>(textview_id) as TextView
textView.gravity = Gravity.CENTER
textView.setLineSpacing(ConvertUtils.dp2px(2f).toFloat(), 1.0f)
根据日志显示,问题就出现在这一行,日志还告诉我们,这行代码把null尝试转换为 TextView:
val textView = mToast?.view?.findViewById<View>(textview_id) as TextView
这里肉眼肯定是看不出来问题了,所以我直接,debug,启动
在经过紧张刺激的调试后,我发现是mToast这个对象的view为空了,接着debug,发现问题出在Toast的makeText()方法中:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
Toast result = new Toast(context, looper);
result.mText = text;
result.mDuration = duration;
return result;
} else {
Toast result = new Toast(context, looper);
View v = ToastPresenter.getTextToastView(context, text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
}
经过反复的调试,发现在我的安卓13的手机上,断点始终会进入下面的判断:
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
Toast result = new Toast(context, looper);
result.mText = text;
result.mDuration = duration;
return result;
}
很明显这里和else的代码相比,是始终没有设置mNextView的,而这个mNextView,就是Toast的view(kotlin写法,view相当于getView和setView两个方法):
@Nullable public View getView() {
return mNextView;
}
没有设置,当然会报空了,我们对比下Android SDK为28的时候的Toast的makeText方法:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
可以看到这个时候是没有那个判断的,所以之前是一直没有问题的,随后在网上找到的文章也应证了我们的猜测:
虽然不知道这个判断的逻辑是什么,但是可以确定的是,我们不能这么用了,只能使用标准的写法,或者按文章里的其他方案:
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
三、用别人造好的轮子,而不是重复造轮子的工作
这时候按照常理,我们只能去自定义一个View了,但是我们的项目中,其实一直有blankj这个工具类框架:
implementation 'com.blankj:utilcodex:1.31.1'
这里我们可以直接使用blankj的ToastUtils类:
ToastUtils.make().setGravity(Gravity.CENTER, 0, 70)
.setTextColor(ColorUtils.getColor(R.color.black_333333)).show(message)
再根据自己的需要调下样式,至此问题解决。