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


R文件位置
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文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
     <public type="layout" name="activity_md1" id="0x7f0a0030" />
</resources>

即我们将某个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<MergeResources> mergeResourcesTask;

可以两个版本这个task都是私有的,不能直接拿到,但既然有这个task,肯定会让我们拿到,否则这个task就没有任何意义了

通过搜索发现
在3.0.1中,提供set和get方法

@Override
@Nullable
public AndroidTask<MergeResources> getMergeResourcesTask() {
    return mergeResourcesTask;
}

@Override
public void setMergeResourcesTask(
@Nullable AndroidTask<MergeResources> 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为

 <public type="layout" name="activity_camera" id="0x7f0a001f" />

结果

 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 type="id" name="music_iv" id="0x7f2c0006" />

编译不过,报错

Public symbol id/music_iv declared here is not defined.

经过百度,说需要在ids.xml中声明相关的id

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <item type="id" name="music_iv" />
</resources>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • ( 第三天打卡) 她是艺术史上第一个女人,有全然鲁莽的真诚以及安静的残忍。在她的艺术里,潜心钻研常见...
    一花亦真阅读 3,398评论 5 8
  • 文/ 杰瑞林 圖/ 杰瑞林 在互換完你最近忙嗎的名片後 我們開始用耍寶與高談闊論 混搭出沒有無奈的空間 此時此刻只...
    字以为阅读 178评论 0 2
  • 昨夜巫山暮霭沉,风翻云涌枕边升。 凤凰锵在琴台卧,笙箫正好江边鸣。 似雪内关软如玉,月下嘤唔斗黄莺。 山外寒山万重...
    古来古来阅读 411评论 0 4