findViewById,ViewHolder,SparseArray<View> viewHolder原理以及效率对比

本文通过实验对比了ViewHolder、SparseArray和findViewById在不同布局复杂度下的性能表现,发现ViewHolder最快,SparseArray在复杂布局中优势明显。
1.SparseArray<View> viewHolder

 SparseArray 这个集合而不是 HashMap ,我们知道 SparseArray 是Android的一个工具类,是官方推荐用来代替 HashMap<Integer,E> 的一个类,它的内部采用了二分查找的实现提高了查找效率。
--用二分法查找到引用。
public class ViewHolderUtil {
    public static <T extends View> T get(View convertView, int id) {
        SparseArray<View> viewHolder = (SparseArray<View>) convertView.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArray<View>();
            convertView.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = convertView.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
}

/**
 * Gets the Object mapped from the specified key, or the specified Object
 * if no such mapping has been made.
 */
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i < 0 || mValues[i] == DELETED) {
        return valueIfKeyNotFound;
    } else {
        return (E) mValues[i];
    }
}
2.findViewById
VIew
protected View findViewTraversal(@IdRes int id) {
    if (id == mID) {
        return this;
    }
    return null;
}

ViewGroup
protected View findViewTraversal(@IdRes int id) {
    if (id == mID) {
        return this;
    }

    final View[] where = mChildren;
    final int len = mChildrenCount;

    for (int i = 0; i < len; i++) {
        View v = where[i];

        if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
            v = v.findViewById(id);

            if (v != null) {
                return v;
            }
        }
    }

    return null;
}
可见采用的是 遍历数组,从而查找到引用
3.ViewHolder
直接就持有引用,不需要查找



So,使用ViewHolder应该是最快的,SparseArray次之。really?做一下实验。

1.先看简单的布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <TextView
        android:layout_width="wrap_content"
        android:text="标题"
        android:textSize="20sp"
        android:id="@+id/title"
        android:layout_height="wrap_content"/>
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="2dp"
        android:background="#000"
        android:layout_height="match_parent"/>
    <TextView
        android:layout_width="wrap_content"
        android:text="内容"
        android:id="@+id/content"
        android:layout_height="wrap_content"/>
</LinearLayout>
代码
public void onClick(View v) {
    long begin;
    long duration;
    switch (v.getId()) {
        case R.id.btn_findviewbyid:
            begin = System.currentTimeMillis();
            doFindViewById();
            duration = System.currentTimeMillis() - begin;
            LogUtils.LOG("btn_findviewbyid" + duration);
            tv_findviewbyid.setText("time:" + duration);
            break;
        case R.id.btn_sparsearray:
            begin = System.currentTimeMillis();
            doSparsearray();
            duration = System.currentTimeMillis() - begin;
            LogUtils.LOG("btn_sparsearray" + duration);
            tv_sparsearray.setText("time:" + duration);
            break;
        case R.id.btn_viewholder:
            begin = System.currentTimeMillis();
            doViewHolder();
            duration = System.currentTimeMillis() - begin;
            LogUtils.LOG("btn_viewholder" + duration);
            tv_viewholder.setText("time:" + duration);

            break;
    }
}

private void doSparsearray() {
    View view = View.inflate(this, R.layout.item_joke, null);
    for (int i = 0; i < COUNT; i++) {
        TextView title = ViewHolderUtil.get(view, R.id.title);
        TextView content = ViewHolderUtil.get(view, R.id.content);
        ImageView imageView = ViewHolderUtil.get(view, R.id.imageView);
        title.setText("title");
        content.setText("content");
    }
}

private void doFindViewById() {
    View view = View.inflate(this, R.layout.item_joke, null);
    for (int i = 0; i < COUNT; i++) {

        TextView title = (TextView) view.findViewById(R.id.title);
        ImageView imageView = (ImageView) view.findViewById(R.id.imageView);
        TextView content = (TextView) view.findViewById(R.id.content);
        title.setText("title");
        content.setText("content");
    }
}

private void doViewHolder() {
    View view = View.inflate(this, R.layout.item_joke, null);
    ViewHolder viewHolder = new ViewHolder();
    viewHolder.title = (TextView) view.findViewById(R.id.title);
    viewHolder.imageView = (ImageView) view.findViewById(R.id.imageView);
    viewHolder.content = (TextView) view.findViewById(R.id.content);
    for (int i = 0; i < COUNT; i++) {
        viewHolder.title.setText("title");
        viewHolder.content.setText("content");
        ImageView imageView = viewHolder.imageView;
    }
}

时间,count 为 10W次:
如果说设置数据的时间(setText)为1088,那么 稀疏数组 查找略快 大概254ms,findviewbyid 大概 400ms
06-23 10:17:53.458 21070-21070/com.example.testeveryting I/CnfolCms: btn_sparsearray1342

06-23 10:17:58.433 21070-21070/com.example.testeveryting I/CnfolCms: btn_findviewbyid1486

06-23 10:18:01.673 21070-21070/com.example.testeveryting I/CnfolCms: btn_viewholder1088

2.看复杂一点的布局
<?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="match_parent">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

        android:orientation="vertical">

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="标题"
            android:textSize="20sp"/>

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="2dp"
            android:layout_height="match_parent"
            android:background="#000"/>

        <TextView
            android:id="@+id/content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="内容"/>

    </LinearLayout>
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

        android:orientation="vertical">

        <TextView
            android:id="@+id/title1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="标题"
            android:textSize="20sp"/>

        <ImageView
            android:id="@+id/imageView1"
            android:layout_width="2dp"
            android:layout_height="match_parent"
            android:background="#000"/>

        <TextView
            android:id="@+id/content1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="内容"/>

    </LinearLayout>

</LinearLayout>

06-23 11:09:09.083 10863-10863/com.example.testeveryting I/CnfolCms: btn_sparsearray2558

06-23 11:09:31.258 10863-10863/com.example.testeveryting I/CnfolCms: btn_findviewbyid3630

06-23 11:09:36.248 10863-10863/com.example.testeveryting I/CnfolCms: btn_viewholder2193


如果说设置数据的时间(setText)为2193,那么 稀疏数组 查找略快 大概365ms,findviewbyid 大概 1437ms



可见,android 4.4.4 mx3手机:布局简单(例如1)的情况下,稀疏数组时间大约是findviewbyid的60%。
     略复杂一点(例如2),稀疏数组时间大约是findviewbyid的25%。在复杂 布局 情况下 节约的时间还是挺可观的。

然而换了一部手机 sansumg s7 android6.0 结果却 很奇怪。 findviewbyid反而时间(例子2)略少。

06-23 11:21:06.546 11228-11228/com.example.testeveryting I/CnfolCms: btn_sparsearray797

06-23 11:21:07.936 11228-11228/com.example.testeveryting I/CnfolCms: btn_findviewbyid794

06-23 11:21:09.616 11228-11228/com.example.testeveryting I/CnfolCms: btn_viewholder682
推测是,计算能力大幅提升的s7 查找时间很快,反而 sparsearray的创建对象的时间 成为了瓶颈(findviewbyid没有创建对象),因此sparseArray查找上节约的一点时间反而被创建对象所需的时间所抵消。



所以,综上,sparseArray在布局复杂需要大量查找的情况下确实能节约很多查找时间,但是可能会因此增加了 创建对象的时间,尤其是在高配置的手机上。
因此为了适配高中低端手机。我觉得最好还是直接使用viewholder,其次sparseArray。
以上实验可能有问题,欢迎指正。


public abstract class HwRecyclerPagerAdapter extends HwPagerAdapter { private static final int CACHE_SIZE = 5; private SparseArray<List<ViewHolder>> caches = new SparseArray<>(); public abstract ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType); public abstract void onBindViewHolder(@NonNull ViewHolder holder, int position); public int getItemViewType(int position) { return 0; } public abstract void onViewRecycled(@NonNull ViewHolder holder); @NonNull @Override public Object instantiateItem(@NonNull ViewGroup viewGroup, int position) { int viewType = getItemViewType(position); //todo 从缓存取 ViewHolder holder = caches.get(viewType).get(0);//fixme 实例代码 if (holder == null) { holder = onCreateViewHolder(viewGroup, position); holder.viewType = viewType; holder.root.setTag(android.R.id.content, holder); } if (holder.root.getParent() != null) { throw new IllegalStateException("ViewHolder views must not be attached when created. "); } onBindViewHolder(holder, position); viewGroup.addView(holder.root); return holder.root; } @Override public void destroyItem(@NonNull ViewGroup viewGroup, int i, @NonNull Object object) { if (!(object instanceof View)) { // log return; } View root = (View) object; ViewHolder holder = (ViewHolder) root.getTag(android.R.id.content); onViewRecycled(holder); List<ViewHolder> queue = caches.get(holder.viewType); if (queue == null) { queue = new ArrayList<>(CACHE_SIZE); } // todo 这里做缓存管理 } public abstract static class ViewHolder { protected int viewType; @NonNull protected View root; public ViewHolder(@NonNull View root) { this.root = root; } } }把上个代码优化成他这样
09-29
public abstract class MyAdapter<T> extends BaseAdapter { private ArrayList<T> mData; private int mLayoutRes; //布局id public MyAdapter() { } public MyAdapter(ArrayList<T> mData, int mLayoutRes) { this.mData = mData; this.mLayoutRes = mLayoutRes; } @Override public int getCount() { return mData != null ? mData.size() : 0; } @Override public T getItem(int position) { return mData.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = ViewHolder.bind(parent.getContext(), convertView, parent, mLayoutRes , position); bindView(holder, getItem(position)); return holder.getItemView(); } public abstract void bindView(ViewHolder holder, T obj); //添加一个元素 public void add(T data) { if (mData == null) { mData = new ArrayList<>(); } mData.add(data); notifyDataSetChanged(); } //往特定位置,添加一个元素 public void add(int position, T data) { if (mData == null) { mData = new ArrayList<>(); } mData.add(position, data); notifyDataSetChanged(); } public void remove(T data) { if (mData != null) { mData.remove(data); } notifyDataSetChanged(); } public void remove(int position) { if (mData != null) { mData.remove(position); } notifyDataSetChanged(); } public void clear() { if (mData != null) { mData.clear(); } notifyDataSetChanged(); } public static class ViewHolder { private SparseArray<View> mViews; //存储ListView 的 item中的View private View item; //存放convertView private int position; //游标 private Context context; //Context上下文 //构造方法,完成相关初始化 private ViewHolder(Context context, ViewGroup parent, int layoutRes) { mViews = new SparseArray<>(); this.context = context; View convertView = LayoutInflater.from(context).inflate(layoutRes, parent, false); convertView.setTag(this); item = convertView; } //绑定ViewHolder与item public static ViewHolder bind(Context context, View convertView, ViewGroup parent, int layoutRes, int position) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(context, parent, layoutRes); } else { holder = (ViewHolder) convertView.getTag(); holder.item = convertView; } holder.position = position; return holder; } @SuppressWarnings("unchecked") public <T extends View> T getView(int id) { T t = (T) mViews.get(id); if (t == null) { t = (T) item.findViewById(id); mViews.put(id, t); } return t; } /** * 获取当前条目 */ public View getItemView() { return item; } /** * 获取条目位置 */ public int getItemPosition() { return position; } /** * 设置文字 */ public ViewHolder setText(int id, CharSequence text) { View view = getView(id); if (view instanceof TextView) { ((TextView) view).setText(text); } return this; } /** * 设置图片 */ public ViewHolder setImageResource(int id, int drawableRes) { View view = getView(id); if (view instanceof ImageView) { ((ImageView) view).setImageResource(drawableRes); } else { view.setBackgroundResource(drawableRes); } return this; } /** * 设置点击监听 */ public ViewHolder setOnClickListener(int id, View.OnClickListener listener) { getView(id).setOnClickListener(listener); return this; } /** * 设置可见 */ public ViewHolder setVisibility(int id, int visible) { getView(id).setVisibility(visible); return this; } /** * 设置标签 */ public ViewHolder setTag(int id, Object obj) { getView(id).setTag(obj); return this; } //其他方法可自行扩展 } }转成kotlin
07-24
可以将 `private final List<MediaViewHolder> imageViewHolderCache = new LinkedList<>();` 和 `private final List<MediaViewHolder> videoViewHolderCache = new LinkedList<>();` 合并为 `private SparseArray<List<MediaViewHolder>> caches = new SparseArray<>();`。`SparseArray` 是 Android 提供的一种特殊的键值对存储结构,使用 `int` 作为键,在存储 `int` 类型键和对象值的场景下,比 `HashMap` 更节省内存,适合用于存储不同类型视图的 `ViewHolder` 缓存。 以下是修改后的代码示例: ```java import android.annotation.SuppressLint; import android.content.Context; import android.net.Uri; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import androidx.annotation.NonNull; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; import androidx.collection.SparseArrayCompat; import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomViewTarget; import com.bumptech.glide.request.transition.Transition; import com.huawei.ascf.AscfFileProvider; import com.huawei.ascf.R; import com.huawei.ascf.core.AppInstance; import com.huawei.ascf.core.AppInstanceManager; import com.huawei.ascf.trace.AscfTrace; import com.huawei.ascf.widget.photoview.PhotoView; import com.huawei.ascf.widget.previewmeida.MediaSource; import com.huawei.ascf.widget.previewmeida.PreviewMediaListener; import com.huawei.ascf.widget.video.AscfVideoView; import com.huawei.ascf.widget.video.VideoUtils; import com.huawei.uikit.hwviewpager.widget.HwPagerAdapter; import java.io.File; import java.util.LinkedList; import java.util.List; @OptIn(markerClass = UnstableApi.class) public class PreviewMediaViewPagerAdapter extends HwPagerAdapter { public static final String TAG = "PreviewMediaViewPagerAdapter"; private final Context context; private final List<MediaSource> mMediaSources; private final PreviewMediaListener viewerListener; private AppInstance mAppInstance; private static final int MAX_CACHE_SIZE = 5; private final SparseArrayCompat<List<MediaViewHolder>> caches = new SparseArrayCompat<>(); public PreviewMediaViewPagerAdapter(@NonNull Context contextInput, @NonNull List<MediaSource> mediaSources, PreviewMediaListener longClickListenerInput) { context = contextInput; mMediaSources = mediaSources; viewerListener = longClickListenerInput; mAppInstance = AppInstanceManager.getInstance(AppInstanceManager.getCurrentAppId()); } @SuppressLint({"ClickableViewAccessibility", "UnsafeOptInUsageError"}) @NonNull @Override public Object instantiateItem(@NonNull final ViewGroup container, final int position) { MediaSource mediaSource = mMediaSources.get(position); if (mediaSource == null) { AscfTrace.e("MediaSource", "MediaSource is null at position " + position); return new View(context); } int viewType = getItemViewType(position); MediaViewHolder holder = onCreateViewHolder(container, viewType); onBindViewHolder(holder, position); container.addView(holder.itemView); return holder.itemView; } public int getItemViewType(int position) { MediaSource mediaSource = mMediaSources.get(position); String type = mediaSource.getType(); if (PreviewMediaConstant.PREMEDIA_IMAGE.equalsIgnoreCase(type)) { return 0; } else if (PreviewMediaConstant.PREMEDIA_VIDEO.equalsIgnoreCase(type)) { return 1; } return -1; } public MediaViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { MediaViewHolder holder = getCachedViewHolder(viewType); if (holder == null) { View view; if (viewType == 0) { view = LayoutInflater.from(context).inflate(R.layout.ascf_media_image_viewer_item, parent, false); } else if (viewType == 1) { view = LayoutInflater.from(context).inflate(R.layout.ascf_media_video_viewer_item, parent, false); } else { AscfTrace.e("MediaSource", "Unknown view type"); view = new View(context); } holder = new MediaViewHolder(view); } return holder; } public void onBindViewHolder(MediaViewHolder holder, int position) { MediaSource mediaSource = mMediaSources.get(position); String type = mediaSource.getType(); String url = mediaSource.getUrl(); String poster = mediaSource.getPoster(); if (PreviewMediaConstant.PREMEDIA_IMAGE.equalsIgnoreCase(type)) { holder.progressBar.setVisibility(View.VISIBLE); loadImageView(url, holder.photoView, holder.progressBar); holder.photoView.setZoomable(false); holder.photoView.setOnPhotoLongClickListener((v, atImage, x, y) -> { if (viewerListener != null) { viewerListener.onLongClick(holder.photoView, position, (int) x, (int) y, url); } }); holder.photoView.setOnPhotoClickListener((view1, x, y) -> { if (viewerListener != null) { viewerListener.onClickListener(holder.photoView, position, (int) x, (int) y, url); } }); } else if (PreviewMediaConstant.PREMEDIA_VIDEO.equalsIgnoreCase(type)) { holder.progressBar.setVisibility(View.VISIBLE); loadVideoView(url, poster, holder.progressBar, holder.ascfVideoView); } } private MediaViewHolder getCachedViewHolder(int viewType) { List<MediaViewHolder> viewHolderList = caches.get(viewType); if (viewHolderList != null && !viewHolderList.isEmpty()) { return viewHolderList.remove(0); } return null; } private void cacheViewHolder(int viewType, MediaViewHolder holder) { List<MediaViewHolder> viewHolderList = caches.get(viewType); if (viewHolderList == null) { viewHolderList = new LinkedList<>(); caches.put(viewType, viewHolderList); } if (viewHolderList.size() < MAX_CACHE_SIZE) { viewHolderList.add(holder); } } private void loadImageView(String url, PhotoView imageView, ProgressBar progress) { Glide.with(imageView).download(url).into(new CustomViewTarget<PhotoView, File>(imageView) { @Override public void onLoadFailed(android.graphics.drawable.Drawable errorDrawable) { progress.post(() -> progress.setVisibility(View.GONE)); } @Override public void onResourceReady(@NonNull File resource, Transition<? super File> transition) { imageView.setPhotoUri(getFileUri(resource), (result, url1) -> progress.post(() -> progress.setVisibility(View.GONE))); } @Override protected void onResourceCleared(android.graphics.drawable.Drawable placeholder) { progress.post(() -> progress.setVisibility(View.GONE)); } }); } private void loadVideoView(String url, String poster, ProgressBar progress, AscfVideoView mAscfVideoView) { VideoUtils.setupPoster(mAscfVideoView, AppInstanceManager.getContainerActivity()); mAscfVideoView.setSrc(VideoUtils.parseUri(context, mAppInstance, url)); if (!poster.isEmpty()) { mAscfVideoView.setPoster(VideoUtils.parseUri(context, mAppInstance, url)); } progress.setVisibility(View.GONE); } private String getFileUri(@NonNull File file) { try { Uri uri = AscfFileProvider.getUriForFile(context, file); return uri.toString(); } catch (Exception ignore) { AscfTrace.i(TAG, "can not get file uri"); } return Uri.fromFile(file).toString(); } @Override public int getCount() { return mMediaSources.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view == object; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object obj) { if (obj instanceof View) { View view = (View) obj; MediaSource mediaSource = mMediaSources.get(position); if (mediaSource != null) { AscfVideoView ascfVideoView = (AscfVideoView) view.getTag(R.id.ascf_video_view); if (ascfVideoView != null) { ascfVideoView.release(); } PhotoView mPhotoView = (PhotoView) view.getTag(R.id.image_view); if (mPhotoView != null) { Glide.with(view).clear(mPhotoView); } container.removeView(view); int viewType = getItemViewType(position); MediaViewHolder holder = new MediaViewHolder(view); onViewRecycled(holder); cacheViewHolder(viewType, holder); } } } public void onViewRecycled(MediaViewHolder holder) { if (holder.ascfVideoView != null) { holder.ascfVideoView.release(); } if (holder.photoView != null) { Glide.with(holder.photoView).clear(holder.photoView); } } class MediaViewHolder { View itemView; ProgressBar progressBar; PhotoView photoView; AscfVideoView ascfVideoView; MediaViewHolder(View itemView) { this.itemView = itemView; this.progressBar = itemView.findViewById(R.id.progress_bar); this.photoView = itemView.findViewById(R.id.image_view); this.ascfVideoView = itemView.findViewById(R.id.ascf_video_view); } } } ``` 在上述代码中,使用 `SparseArrayCompat<List<MediaViewHolder>> caches` 来存储不同类型的 `MediaViewHolder` 缓存。`getCachedViewHolder` 方法用于从缓存中获取 `MediaViewHolder`,`cacheViewHolder` 方法用于将 `MediaViewHolder` 存入缓存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值