美文网首页
RemoteView 资源id 引发的错误

RemoteView 资源id 引发的错误

作者: sofarsogoo_932d | 来源:发表于2018-08-08 23:28 被阅读0次

    场景

    在版本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>
    

    相关文章

      网友评论

          本文标题:RemoteView 资源id 引发的错误

          本文链接:https://www.haomeiwen.com/subject/grwyeftx.html