这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天
自定义通知系列文章包括:
- 自定义通知的基础使用、自定义通知样式的UI适配(展开&折叠)
- bug修复,包括TransactionTooLargeException、ANR
本文是第二篇,记录了TTL问题以及解决思路和方案,值得一看~
TransactionTooLargeException
报错如下
Fatal Exception: java.lang.RuntimeException
android.os.TransactionTooLargeException: data parcel size 518960 bytes
调用notify方法报错,就一行代码
(ContextCompat.getSystemService(context, NotificationManager::class.java) as NotificationManager).notify(
NotifyConstant.NOTIFY_ID_RESIDENT,
getResidentNotification(context)
)
第一次尝试:
看getResidentNotification方法中有没有大图传输或者intent传输值,唯一和图片相关的就是
setLargeIcon和setSmallIcon,尝试将图片压缩到200K以下,线上仍然报错,推测和图片无关
第二次尝试:
问题非必现的,刚上线没有这个问题,时间越往后问题占比越高,结合项目的特点:通知一直运行在后台且每隔5分钟刷新一次界面,考虑和时间的积累有关,尝试修改刷新频率为5ms(之前是5min),进行极限测试,问题复现,初步判断和更新UI有关。刷新UI调用的代码如下,考虑和RemoteViews有关,且是跨进程通信出现的传输数据过大,具体查看更新UI的代码
remoteView?.setTextViewText(R.id.tv_cpu_tem_tip_yellow,"")
remoteView.setViewVisibility(R.id.small_fl_cpu, View.GONE)
系统并没有通过Binder去直接支持View的跨进程访问,而是提供了一个Action的概念,Action代表一个View操作,Action同样实现了Parcelable接口。系统首先将View操作封装到Action对象,界面上的控件每更新一次,mActions的长度就会加1,调用NotificationManager.notify()就会遍历所有的Action,执行Action的apply方法,通过反射调用TextView的相关方法。
(图片引用自安卓开发艺术探索)
//调用setTextViewText后,mActions会+1
private ArrayList<Action> mActions;
private void addAction(Action a) {
if (mActions == null) {
mActions = new ArrayList<>();
}
mActions.add(a);
}
//BaseReflectionAction.java
@Override
public final void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class<?> param = getParameterType(this.type);
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
Object value = getParameterValue(view);
//调用setText方法
try {
getMethod(view, this.methodName, param, false /* async */).invoke(view, value);
} catch (Throwable ex) {
throw new ActionException(ex);
}
}
考虑跨进程通信时,mActions对象过大,导致抛出TTL,解决:mActions的大小超过一定限制就重新初始化RemoteView,伪代码如下
private var mActionsSize = 0
private var mRefreshTime = 0
mRefreshTime++
runCatching {
val remoteViewsClass = Class.forName("android.widget.RemoteViews")
val mActionsField: Field = remoteViewsClass.getDeclaredField("mActions")
mActionsField.isAccessible = true
//反射拿到mActions的大小
val d = mActionsField.get(residentRemoteView) as MutableList<*>
mActionsSize = d.size
}
//这里有一个兜底逻辑,如果反射获取mActionsSize失败,就走mRefreshTime的逻辑
//mRefreshTime是指调用RemoteViews API的次数
// 100 和 15是一个粗略值,大佬有更好的建议请在文末留言,谢谢啦~
if (mActionsSize >= 100 || mRefreshTime >= 15) {
mActionsSize = 0
mRefreshTime = 0
residentRemoteView = null //手动将RemoteView置null
residentSmallRemoteView = null
}
if (residentRemoteView == null || residentSmallRemoteView == null) {
initResidentRemoteView(context)
}
通知的覆盖问题(探索性问题)
需求:
当用户卸载应用时,更晚的弹出问题,这样用户最后看到的就是我们的app,下拉通知栏,我们的应用就会显示在第一个。
思路:
1.提高通知的优先级,目前项目代码里已经是PRIORITY_MAX
2.使用window方式显示在屏幕中间,更具有干扰性
3.尝试在展示通知之前开个delay 10秒,log确实是有延迟了10秒,但是在通知栏里面,那条通知并没有置顶显示,此方案不可行