RemoteView 资源id 引发的错误

场景

在版本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的排序是按照字母顺序来排列的


RemoteView 资源id 引发的错误_第1张图片
R文件位置
RemoteView 资源id 引发的错误_第2张图片
R文件.png

并且两种类型的资源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



   

你可能感兴趣的:(RemoteView 资源id 引发的错误)