场景
在版本1.0时,弹出了一个通知栏
此时,应用升级到2.0,再去点击这个通知栏会报错
原因分析
RemoteView在使用自定义布局,会用到一些资源,如layout,drawable,string等等,这些资源其实都在R文件中以一个int型的整数保存着
当我们的应用更新时,可能增加了一些资源,此时R文件中的资源id会重新排序,此时的id和旧版本的id可能就不同了
因此此时去点击旧版本的通知栏可能就会报错,找不到对应的资源
RemoteView和app不是同一个应用,因此app的资源的改变了,RemoteView是不知道的
错误复现
通知类
public class NotifyUtil {
public static void showNotify(Context context) {
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
RemoteViews remoteViews=new RemoteViews(context.getPackageName(), R.layout.layout_notify);
remoteViews.setImageViewResource(R.id.music_iv,R.drawable.ic_launcher_background);
builder.setSmallIcon(R.mipmap.ic_launcher);//设置小图标,不设置会报错
//跳转
Intent intent = new Intent(context, AnimActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
//设置通知栏属性
builder.setTicker("通知来啦")
.setAutoCancel(true)
// .setDefaults(NotificationCompat.DEFAULT_ALL) ///打开呼吸灯,声音,震动,触发系统默认行为
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContent(remoteViews)
.setContentIntent(pendingIntent);
manager.notify(1, builder.build());
}
}
从代码中,我们看到有一个通知栏的布局R.layout.layout_notify
找到R文件,该布局的id
public static final int layout_notify=0x7f09001d;
查看R文件,就可以看出R文件里面id的排序是按照字母顺序来排列的
并且两种类型的资源id 相差很多
integer的起始id是0x7f080000
layout的起始id是0x7f090000
相差0x10000,换算成十进制就是16的4次方=65536
同种类型的资源id只相差1
因此得出结论,影响资源id重排的是同种类型的资源
增加一个layout资源
public static final int laayout_notify=0x7f09001d;
public static final int layout_notify=0x7f09001e;
可以看出资源原本的资源id变换了
覆盖安装新的apk
令人遗憾的通知栏消失了
先暂时模拟一下吧
RemoteViews remoteViews=new RemoteViews(context.getPackageName(), 0x7f09001d);
即用之前的布局的id来模拟这种情况
public static final int laayout_notify=0x7f09001d;
public static final int layout_notify=0x7f09001e;
然后点击开启通知栏,直接崩溃,因为该id的布局已经不是刚才的布局了
崩溃报错
Bad notification posted from package com.sf.appdemo: Couldn't expand RemoteViews for:
StatusBarNotification(pkg=com.sf.appdemo user=UserHandle{0} id=1 tag=null key=0|com.sf.appdemo|1|null|10157: Notification(pri=2 contentView=com.sf.appdemo/0x7f040000 vibrate=null sound=null tick defaults=0x0 flags=0x10 color=0x00000000 vis=PRIVATE))
解决办法
1. 给通知栏的相关资源都加上前缀aaa,让它本来就在前面
这种方法需要对所有通知栏用到的资源都加上前缀,并且也不能100%保证,万一别人有一个资源文件的前缀是aaaa了
2. 固定资源id
参考链接
https://github.com/ceabie/AndroidPublicXmlCompat
注意
下列操作的gradle插件版本3.0.1
发现在gradle 3.1.2版本一些方法变了,编译不过了
https://stackoverflow.com/questions/49713707/when-the-android-gradle-plugin-update-to-3-1-0-from-3-0-1-error-happened-gradle
第一步,在根目录下配置一个public-xml.gradle文件
afterEvaluate {
for (variant in android.applicationVariants) {
def scope = variant.getVariantData().getScope()
String mergeTaskName = scope.getMergeResourcesTask().name
def mergeTask = tasks.getByName(mergeTaskName)
println "public-xml:"+mergeTaskName
mergeTask.doLast {
copy {
int i=0
from(android.sourceSets.main.res.srcDirs) {
include 'values/public.xml'
rename 'public.xml', (i++ == 0? "public.xml": "public_${i}.xml")
}
into(mergeTask.outputDir)
}
}
}
}
第二步,在需要用到的模块的value文件下增加一个public.xml文件
即我们将某个Activcity的布局文件的id 固定为0x7f0a0030
这里对id的定义说明一下
前两位一定是7f,即使写成7e,在R文件中也是7f
7f的后面两位标识资源类型,相同的资源必须相同,不同类型的资源必须不同,否则编译不过
第三步,在模块对应的build.gradle 中增加
apply from: rootProject.file('public-xml.gradle')
第四步,在gradle.properties文件中禁用aapt2
# 关闭aapt2
android.enableAapt2=false
第五步,验证是否起作用
找到R文件,查看activity_md1的id
public static final int activity_main=0x7f0a002f;
public static final int activity_md1=0x7f0a0030;
public static final int activity_md2=0x7f0a0031;
增加一个layout文件activity_md0
public static final int activity_main=0x7f0a002f;
public static final int activity_md0=0x7f0a0031;
public static final int activity_md1=0x7f0a0030;
public static final int activity_md2=0x7f0a0032;
可以看出activity_md1已经被固定住了
关于3.1.2 编译不过的问题,我最终找到了答案(查看源码)
如何查看gradle插件源码
当我们在写自定义gradle插件时,在插件项目下添加依赖
compile 'com.android.tools.build:gradle:3.1.2'
3.0.1和3.1.2的gradle源码对比
在看源码之前,我们先拆解一下public-xml.gradle中用到的类
可与通过print打印出具体的类
def scope = variant.getVariantData().getScope()
println "scope:"+scope
String mergeTaskName = scope.getMergeResourcesTask().name
println "mergeTaskName:"+mergeTaskName
def mergeTask = tasks.getByName(mergeTaskName)
println "mergeTask:"+mergeTask
输出(在控制台输入./gradlew即可)
scope:VariantScopeImpl{debug}
mergeTaskName:mergeDebugResources
mergeTask:task ':packages:app:mergeDebugResources'
scope:VariantScopeImpl{release}
mergeTaskName:mergeReleaseResources
mergeTask:task ':packages:app:mergeReleaseResources'
从variant in android.applicationVariants开始
这里的variant则对应的ApplicationVariantImpl
getVariantData()则对应ApplicationVariantData
getScope()则对应VariantScopeImpl
这里我们关注一下VariantScopeImpl类
在里面我们找到资源合并的task
//3.1.2
@Nullable private MergeResources mergeResourcesTask;
//3.0.1
@Nullable private AndroidTask mergeResourcesTask;
可以两个版本这个task都是私有的,不能直接拿到,但既然有这个task,肯定会让我们拿到,否则这个task就没有任何意义了
通过搜索发现
在3.0.1中,提供set和get方法
@Override
@Nullable
public AndroidTask getMergeResourcesTask() {
return mergeResourcesTask;
}
@Override
public void setMergeResourcesTask(
@Nullable AndroidTask mergeResourcesTask) {
this.mergeResourcesTask = mergeResourcesTask;
}
然后通过name拿到了真正的MergeResources
但是在3.1.2中,不在有AndroidTask,直接变成了MergeResources,但是并没有提供相关的get方法
但是在该类中发现了另一个类BaseVariantData
public MergeResources mergeResourcesTask;
这个是public的,可以直接获取到
现在的问题就是怎么拿到这个BaseVariantData,好在的是该类提供了get方法来拿到它
@Override
@NonNull
public BaseVariantData getVariantData() {
return variantData;
}
即改造一下即可得到mergeResourcesTask
//3.0.1的写法
def scope = variant.getVariantData().getScope()
String mergeTaskName = scope.getMergeResourcesTask().name
def mergeTask = tasks.getByName(mergeTaskName)
//3.1.2的写法
def scope = variant.getVariantData().getScope()
def mergeTask = scope.getVariantData().mergeResourcesTask
通过看BaseVariantData的继承关系
发现ApplicationVariantData就是继承它,而这个类在getVariantData()这一步就可以获得,因此可以简化一下上面的写法
def mergeTask = variant.getVariantData().mergeResourcesTask
该写法在3.0.1和3.1.2中均可以
现在贴一下完整的写法
afterEvaluate {
for(variant in android.applicationVariants){
def mergeTask = variant.getVariantData().mergeResourcesTask
println variant.getVariantData()
mergeTask.doLast {
copy {
int i=0
from(android.sourceSets.main.res.srcDirs) {
include 'values/public.xml'
rename 'public.xml', (i++ == 0? "public.xml": "public_${i}.xml")
}
into(mergeTask.outputDir)
}
}
}
}
经过测试,资源id的固定已经生效
关于application和library中R文件的问题
假如我有一个Library项目,包名为com.sf.libplayer
发现在application对应的library包下面,也会生成一份R文件,并且两者的R文件中的资源id不一样
//library本身的id
public static int activity_camera = 0x7f0f001b;
//application里面的id
public static final int activity_camera = 0x7f0a001d;
那么最终运行的时候到底以哪个为准了,我们做个测试
先用library的id
Caused by: android.content.res.Resources$NotFoundException: File from xml type layout resource ID #0x7f0f001b
再用application中的id
setContentView(0x7f0a001d);
成功运行
结论
library中的资源id,通过编译后,最终都会在application生成一份R文件,并且里面的id才是正确的id
通过这个结论
我们在做资源id固定的问题时,只需要在application下面的value文件下写一份public.xml即可,无需针对每个library
我们在做一个测试,固定一下这个id为
结果
setContentView(0x7f0a001d); //程序崩溃
setContentView(0x7f0a001f); //正常运行
到这已经很明了了
只需要找到项目中所有需要固定id的资源文件,放入public.xml即可
在实际过程中,又遇到了一个问题
不同布局中不同的view用着相同的id的问题
public static final int music_iv=0x7f0a0096;
试着用该id来绑定
remoteViews.setImageViewResource(0x7f0a0096,R.drawable.ic_launcher_background);
music_iv=findViewById(0x7f0a0096);
music_iv.setImageResource(R.drawable.music_playing);
在固定找个view的id测试一下
编译不过,报错
Public symbol id/music_iv declared here is not defined.
经过百度,说需要在ids.xml中声明相关的id