我们最习惯的ID设置方式大概就是这样了,在xml中直接使用android:id为一个View添加ID。但是这并不能解决所有的问题,我们有很多的View都是动态创建,还有的是自定义控件生成的,例如我们要在代码里实现相对布局,那我们就必须拿到相对的基准View的ID,但是这样就没办法直接使用xml进行定义了。
不过我想大家都知道就想我们通常能够用view.getId()一样获取到View的ID,我们也能够用view.setId(int id)来为View设定ID。不过这就出现了一个很严重的问题ID是个int类型的数字,那我们什么都可以设置吗?
ID的使用方式
随便的打开一个Android工程的R文件我们都能看到下面的场景:
public final class R {
public static final class anim {
public static final int abc_fade_in=0x7f050000;
public static final int abc_fade_out=0x7f050001;
public static final int abc_grow_fade_in_from_bottom=0x7f050002;
public static final int abc_popup_enter=0x7f050003;
public static final int abc_popup_exit=0x7f050004;
public static final int abc_shrink_fade_out_from_bottom=0x7f050005;
public static final int abc_slide_in_bottom=0x7f050006;
public static final int abc_slide_in_top=0x7f050007;
public static final int abc_slide_out_bottom=0x7f050008;
public static final int abc_slide_out_top=0x7f050009;
public static final int course_in=0x7f05000a;
public static final int course_item_in=0x7f05000b;
public static final int course_item_out=0x7f05000c;
public static final int course_out=0x7f05000d;
public static final int dialog_fade_in_center=0x7f05000e;
public static final int dialog_fade_out_center=0x7f05000f;
public static final int dialog_slide_in_bottom=0x7f050010;
public static final int dialog_slide_in_top=0x7f050011;
public static final int dialog_slide_out_bottom=0x7f050012;
public static final int dialog_slide_out_top=0x7f050013;
...
每一个我们的资源文件,无论是动画,图片,View,全部都在R文件中有一个对应的16进制的数据与之对应,我们向下看就能看到我们为View注册的ID。
public static final int TwoSideStyleRangeSlider_pressedTargetRadius = 3;
/**
This symbol is the offset where the {@link com.lfk.classtabledemo.R.attr#startMax}
attribute's value can be found in the {@link #TwoSideStyleRangeSlider} array.
Must be an integer value, such as "100".
This may also be a reference to a resource (in the form
"@[package:]type:name") or
theme attribute (in the form
"?[package:][type:]name")
containing a value of this type.
@attr name com.lfk.classtabledemo:startMax
*/
public static final int TwoSideStyleRangeSlider_startMax = 8;
这两个是一个双向滚动条的styleable的定义数据,我们观察到我们自己的定义数据的十六进制数是非常的小的,这里我注册的少甚至是个位数,那我们在手动注册的时候就更不能轻举妄动了,因为一不小心我们可能就会重复定义两个View到一个ID上,这种未定义的行为的后果也是未知的。并且IDE也会在你试图手动设置一个int值的时候给你警告。
方案一:把申请到的id从xml传入程序中
这是一种比较取巧的办法,我们可以把申请对应View的ID写进整个自定义控件的styleable参数里面然后当成参数传入。
效果图是这样的:

我们把icon的ID通过注册一个参数传入,在代码中设置达到使用相对布局的效果。
iconID = array.getResourceId(R.styleable.TwoSideEditorMenuLine_iconID, 0);
labelID = array.getResourceId(R.styleable.TwoSideEditorMenuLine_labelID, 0);
在布局中获取然后设置。
方法二:View.generateViewId();
Google在API17之后终于意识到了这个问题,所以此后的View源码中就多了几个方法和动态id相关的方法。
/**
* Sets the identifier for this view. The identifier does not have to be
* unique in this view's hierarchy. The identifier should be a positive
* number.
*
* @see #NO_ID
* @see #getId()
* @see #findViewById(int)
*
* @param id a number used to identify the view
*
* @attr ref android.R.styleable#View_id
*/
public void setId(@IdRes int id) {
mID = id;
if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
mID = generateViewId();
}
}
比如设置Id这个方法,17之前是没有这个if语句的,这个if语句就是说如果你给他设置了一个NO_ID系统默认是-1的值,View就会自动用generateViewId()来给自己申请一个ID,所以说直接写:
就可以自动注册ID了。
从这里我们可以看出实际上的调用方法是View.generateViewId();,但是上面的注释里面出现了一句值得注意的话:
Sets the identifier for this view. The identifier does not have to be
unique in this view’s hierarchy. The identifier should be a positive
number.
从这里我们知道了其实View的id并不一定是要完全相同的在View树中,只要identifier是正数就好了。但是我还是建议能有一个唯一的id给View,因为findViewById时对View树进行了depth-first遍历,很可能找到的不是我们想要的View。
方法三:看看generateViewId()的源码是怎么实现的
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
/**
* Generate a value suitable for use in {@link #setId(int)}.
* This value will not collide with ID values generated at build time by aapt for R.id.
*
* @return a generated ID value
*/
public static int generateViewId() {
for (;;) {
final int result = sNextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
}
因为很多时候我们项目的minSDK版本都小于17,这个方法可能根本就不能用。所以我们来看一下generateViewId()方法的具体实现。
首先代码里出现了一个常量0x00ffffff这是系统为View的Id分配的一个最大常数,明显看到,超过就归一,重新开始排序,不过这个常数的十进制大概有160万多,普通应用并不容易超过。
其中的AtomicInteger,从名字也能看出这是一个整形变量,和普通的Integer的区别就是AtomicInteger是线程安全的,因为它的增减全是靠硬件实现的原子指令,所以不需要关键字或是加锁就能实现线程安全。
所以说看来在源码中还考虑到了大量View同时申请ID时候的并发问题,sNextGeneratedId.compareAndSet(result, newValue)是一个CAS操作,CAS就是这个函数名的一个缩写,这是保证数据在并发情况下保持完整性的一种无锁算法,加锁会带来很多性能问题,所以CAS采用了比较设定的方式实现算法,从程序中可以看到result和newValue其实的值应该是一样的,同时传入就是为了在使用的时候,就是说我们把这个result返回的时候,比较这两个值,因为一段程序可能由多次进入,所以变量的值可能会修改,CAS的比较就是使用原值的备份,进行比较,当返回时,如果比较相同就修改成新的数值,不同就不改变。
所以我们可以把这个静态方法拷贝出来,放到工具类里,给AtomicInteger设置一个较大的起始值。
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(2000);
就能使用了。