修复一个 BaseRecyclerViewAdapterHelper 漏洞
- 2021 年 11 月 25 日
本文字数:5746 字
阅读完需:约 19 分钟
1.问题
BaseRecyclerViewAdapterHelper 简介:强大而灵活的 RecyclerView Adapter,就是封装了若干 RecyclerView 的适配器的接口和方法,从而简化 Adapter 的实现,提升应用的开发效率,是一个比较流行的项目。
https://github.com/CymChad/BaseRecyclerViewAdapterHelper
导入 BaseRecyclerViewAdapterHelper 方法:
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'
存在的问题:在 Adapter 的 convert 方法中,调用 BaseViewHolder.setText 方法报了 NullPointException 错误。
2.例程
自定义 Adapter 源码:
public class RecordAdapter extends BaseQuickAdapter<RecordBean, BaseViewHolder> {
private static final int ITEM_WIDTH = (int) AppInfoProvider.dipToPixels(150);
public RecordAdapter(int layoutResId, @Nullable List<RecordBean> data) {
super(layoutResId, data);
}
@Override
protected void convert(BaseViewHolder helper, RecordBean item) {
helper.setText(R.id.nick_name, item.getUserName());
// helper.setText(R.id.chat_from_voice_length, String.valueOf(item.getFileName())); // 错误代码,NullPointException
int type = helper.getItemViewType();
ViewGroup chatContentLayout = helper.getView(R.id.chat_content_layout);
if (type == 0) {
if (null != chatContentLayout) {
View progress = chatContentLayout.findViewById(R.id.progress);
if (null != progress)
progress.setVisibility(View.GONE);
TextView textView = chatContentLayout.findViewById(R.id.chat_from_voice_length);
if (null != textView) {
textView.getLayoutParams().width = ITEM_WIDTH;
textView.requestLayout();
textView.setText(String.valueOf(item.getFileName()));
}
View chatFromVoiceAnim = chatContentLayout.findViewById(R.id.chat_from_voice_anim);
if (null != chatFromVoiceAnim)
chatFromVoiceAnim.setVisibility(View.GONE);
View chatFromWarpView = chatContentLayout.findViewById(R.id.chat_from_warp_view);
if (null != chatFromWarpView)
chatFromWarpView.setOnClickListener(v -> {
if (getOnItemChildClickListener() != null) {
getOnItemChildClickListener().onItemChildClick(RecordAdapter.this, v, helper.getLayoutPosition());
}
});
}
}else {
if (null != chatContentLayout) {
View progress = chatContentLayout.findViewById(R.id.voice_progress);
if (null != progress)
progress.setVisibility(View.GONE);
View unreadImgView = chatContentLayout.findViewById(R.id.unread_img_view);
if (null != unreadImgView)
unreadImgView.setVisibility(View.GONE);
TextView textView = chatContentLayout.findViewById(R.id.chat_to_voice_length);
if (null != textView) {
textView.getLayoutParams().width = ITEM_WIDTH;
textView.requestLayout();
textView.setText(String.valueOf(item.getFileName()));
}
View chatFromVoiceAnim = chatContentLayout.findViewById(R.id.chat_to_voice_anim);
if (null != chatFromVoiceAnim)
chatFromVoiceAnim.setVisibility(View.GONE);
View chatToWarpView = chatContentLayout.findViewById(R.id.chat_to_warp_view);
if (null != chatToWarpView)
chatToWarpView.setOnClickListener(v -> {
if (getOnItemChildClickListener() != null) {
getOnItemChildClickListener().onItemChildClick(RecordAdapter.this, v, helper.getLayoutPosition());
}
});
}
}
}
}
BaseViewHolder 对应的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/time_tv"
style="@style/ChattingUISystem"
android:layout_gravity="center_horizontal"
android:layout_marginTop="5dp"
android:background="@drawable/chatsystem_bg"
android:gravity="center" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:orientation="horizontal"
android:padding="3dip"
android:layout_marginHorizontal="@dimen/item_horizontal_margin" >
<ImageView
android:id="@+id/chat_head_iv"
style="@style/chat_head_tv"
android:layout_alignParentRight="true" />
<TextView
android:id="@+id/nick_name"
style="@style/chat_from_nick_name" />
<RelativeLayout
android:id="@+id/chat_content_layout"
android:layout_width="275dip"
android:layout_height="wrap_content"
android:layout_below="@+id/nick_name"
android:layout_toLeftOf="@+id/chat_head_iv" >
<LinearLayout
android:id="@+id/chat_from_warp_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginRight="4dp"
android:background="@drawable/chat_from_warp_bg"
android:backgroundTint="@color/chat_from_bg_color"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="horizontal" >
<LinearLayout
android:id="@+id/chat_from_voice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="left"
android:orientation="horizontal" >
<ImageView
android:id="@+id/chat_from_voice_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/voice_paly_right_3" />
<ImageView
android:id="@+id/chat_from_voice_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@drawable/voice_play_right" />
</LinearLayout>
<TextView
android:id="@+id/chat_from_voice_length"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/SmallerTextSize" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress"
style="@android:style/Widget.ProgressBar.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="3dp"
android:layout_toLeftOf="@id/chat_from_warp_view" />
<ImageView
android:id="@+id/failed_img_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="3dp"
android:layout_toLeftOf="@id/chat_from_warp_view"
android:background="@drawable/im_send_failed_bg"
android:visibility="gone" />
<ImageView
android:id="@+id/imageview_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="1dp"
android:layout_marginTop="3dp"
android:layout_toLeftOf="@id/chat_from_warp_view"
android:background="@drawable/send_to"
android:visibility="gone"
/>
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
3.原理分析
首先查看 BaseViewHolder.setText 方法:
// viewId = R.id.chat_from_voice_length,我们想找到上面layout中的chat_from_voice_length的TextView
public BaseViewHolder setText(@IdRes int viewId, CharSequence value) {
TextView view = getView(viewId);
view.setText(value);
return this;
}
继续查看 BaseViewHolder.getView 方法:
public <T extends View> T getView(@IdRes int viewId) {
View view = views.get(viewId); // 可以看到这是一个缓存机制,仅在第一次时查找viewId对应的控件
if (view == null) {
view = itemView.findViewById(viewId); // 在itemView中查询R.id.chat_from_voice_length
views.put(viewId, view);
}
return (T) view;
}
继续查看 itemView 是怎么生成的
public abstract static class ViewHolder {
@NonNull
public final View itemView; // 可以看到 itemView 是RecyclerView.ViewHolder中的成员
...
public ViewHolder(@NonNull View itemView) { // 并且在BaseViewHoler实例化的时候会传递进来
if (itemView == null) {
throw new IllegalArgumentException("itemView may not be null");
}
this.itemView = itemView;
}
}
回到 BaseQuickAdapter 中:
protected K createBaseViewHolder(ViewGroup parent, int layoutResId) {
return createBaseViewHolder(getItemView(layoutResId, parent));
}
protected View getItemView(@LayoutRes int layoutResId, ViewGroup parent) {
return mLayoutInflater.inflate(layoutResId, parent, false);
}
由上面分析可知,itemView.findViewById 其实相当于 View.findViewById
// View.java
@Nullable
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this; // 即表示 itemView只能找到自己,或者这个方法是一个虚拟的,可被子类重写
}
return null;
}
查找 View 的子类,itemView 其实应该是一个 ViewGroup,
// ViewGroup.java
@Override
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) { // 遍历当前ViewGroup所有子View,并递归查找,直到找到为止
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return (T) v;
}
}
}
return null;
}
那么问题来了,由于面向对象编程的多态特性,在执行 View.findViewTraversal 时,理论上会调用子类 ViewGroup.findViewTraversal 方法,在 layout 文件中,应该可以找到 R.id.chat_from_voice_length 才对。那为什么还会报错呢?
版权声明: 本文为 InfoQ 作者【Changing Lin】的原创文章。
原文链接:【http://xie.infoq.cn/article/9a319e2803b3ff003211bd8c8】。文章转载请联系作者。
Changing Lin
获得机遇的手段远超于固有常规之上~ 2020.04.29 加入
我能做的,就是调整好自己的精神状态,以最佳的面貌去面对那些未曾经历过得事情,对生活充满热情和希望。
评论