使用DPAD快速滚动时,RecyclerView的onCreateViewHolder方法被过度调用

22

我正在开发适用于Amazon Fire TV的应用程序。

由于这是一个电视应用程序(没有触摸屏),因此我需要在行布局中添加可聚焦元素以便能够导航。

我使用了一个非常简单的Recyclerview,其中包含图像、文本和一个可聚焦元素。当我向上或向下滚动时,一切都可以正确地滚动,但是当我比滚动速度更快地导航时,它会创建新的视图持有者(在屏幕外)并使UI变得缓慢。

我创建了一个带有创建数字的活动。当我慢慢滚动时,最高的创建号码是10。但是当我快速滚动时,我会在一秒钟内获得创建数字为60的卡片。这会导致巨大的延迟,应用程序会丢失很多帧。我的方法完全错误吗?

使用下面的代码进行测试。

/**
 * Created by sylversphere on 15-04-15.
 */
public class LandfillActivity extends Activity{

private Context context;

private static int ticketNumber;
private static int getTicket(){
    ticketNumber ++;
    return ticketNumber;
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    context = this;
    setContentView(R.layout.landfill_activity);
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    GridLayoutManager glm = new GridLayoutManager(context, 2);
    recyclerView.setLayoutManager(glm);
    SickAdapter sickAdapter = new SickAdapter();
    recyclerView.setAdapter(sickAdapter);
}

public class SickViewHolder extends RecyclerView.ViewHolder{
    TextView ticketDisplayer;
    public ImageView imageView;
    public SickViewHolder(View itemView) {
        super(itemView);
        ticketDisplayer = (TextView) itemView.findViewById(R.id.ticketDisplayer);
        imageView = (ImageView) itemView.findViewById(R.id.imageView);

        itemView.findViewById(R.id.focus_glass).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                context.startActivity(new Intent(context, LouisVuittonActivity.class));
            }
        });
    }
    public void setTicket(int value){
        ticketDisplayer.setText(""+value);
    }
}

public class SickAdapter extends RecyclerView.Adapter<SickViewHolder>{

    @Override
    public SickViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        SickViewHolder svh = new SickViewHolder(getLayoutInflater().inflate(R.layout.one_row_element, null));
        svh.setTicket(getTicket());
        return svh;
    }

    @Override
    public void onBindViewHolder(SickViewHolder holder, int position) {
        String[] image_url_array = getResources().getStringArray(R.array.test_image_urls);
        Picasso.with(context).load(image_url_array[position % image_url_array.length] ).fit().centerCrop().into(holder.imageView);
    }

    @Override
    public int getItemCount() {
        return 100000;
    }
}
}

one_row_element.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:orientation="horizontal">
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:adjustViewBounds="true"
            android:scaleType="centerCrop"
            android:src="@mipmap/sick_view_row_bg" />
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left|center_vertical"
            android:layout_marginLeft="15dp"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/virusTextView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Creation #"
                android:textColor="#fff"
                android:textSize="40sp" />
            <TextView
                android:id="@+id/ticketDisplayer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="1"
                android:textColor="#fff"
                android:textSize="40sp" />
        </LinearLayout>
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/focus_glass"
            android:background="@drawable/subtle_focus_glass"
            android:focusable="true"
            android:focusableInTouchMode="true"/>
    </FrameLayout>
</FrameLayout>

test_image_urls.xml(非我所有的URL)

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="test_image_urls"
    formatted="false">
    <item>http://farm4.static.flickr.com/3175/2737866473_7958dc8760.jpg</item>
    <item>http://farm4.static.flickr.com/3276/2875184020_9944005d0d.jpg</item>
    <item>http://farm3.static.flickr.com/2531/4094333885_e8462a8338.jpg</item>
    <item>http://farm4.static.flickr.com/3289/2809605169_8efe2b8f27.jpg</item>
    <item>http://2.bp.blogspot.com/_SrRTF97Kbfo/SUqT9y-qTVI/AAAAAAAABmg/saRXhruwS6M/s400/bARADEI.jpg</item>
    <item>http://fortunaweb.com.ar/wp-content/uploads/2009/10/Caroline-Atkinson-FMI.jpg</item>
    <item>http://farm4.static.flickr.com/3488/4051378654_238ca94313.jpg</item>
    <item>http://farm4.static.flickr.com/3368/3198142470_6eb0be5f32.jpg</item>
    <item>http://www.powercai.net/Photo/UploadPhotos/200503/20050307172201492.jpg</item>
    <item>http://www.web07.cn/uploads/Photo/c101122/12Z3Y54RZ-22027.jpg</item>
    <item>http://www.mitravel.com.tw/html/asia/2011/Palau-4/index_clip_image002_0000.jpg</item>
    <item>http://news.xinhuanet.com/mil/2007-05/19/xinsrc_36205041914150623191153.jpg</item>
    <item>http://ib.berkeley.edu/labs/koehl/images/hannah.jpg</item>
    <item>http://down.tutu001.com/d/file/20110307/ef7937c2b70bfc2da539eea9df_560.jpg</item>
    <item>http://farm3.static.flickr.com/2278/2300491905_5272f77e56.jpg</item>
    <item>http://www.pic35.com/uploads/allimg/100526/1-100526224U1.jpg</item>
    <item>http://img.99118.com/Big2/1024768/20101211/1700013.jpg</item>
    <item>http://farm1.static.flickr.com/45/139488995_bd06578562.jpg</item>
</string-array>
</resources>
细微聚焦。
    <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_focused="true" android:drawable="@color/glass_focus"/>
    <item android:drawable="@color/glass_normal"/>
</selector>

glass_normal的值为#9000

glass_focus的值为#0000


1
可能是因为在您的onBindViewHolder()中图像加载需要很长时间。我没有使用过Picasso,所以不确定是否可能,但我所做的是在我的recycler中异步延迟加载图像(我使用Universal Image Loader,在其中肯定是可能的)。这解决了平滑滚动和创建太多视图的问题。这只是一个想法,可能有助于您,但值得一试。 - kha
@kha 我也尝试过这种方法。它确实有点用,但我不得不至少延迟700毫秒,这使得即使是缓存的图像也加载缓慢。然后就出现了问题。 :( - Dreamingwhale
就像我说的那样,我不确定现在是否仍然是这种情况。我相信谷歌现在已经解决了这个问题。 - Bojan Kseneman
1
@BojanKseneman,不确定您从哪里得到的5到5k职位信息,但那是完全错误的信息,这从未发生过。 @Dreamingwhale,由于瞬态状态,您可能会失去视图持有者,请在适配器中覆盖onFailedToRecycleView并查看是否收到了调用。 - yigit
1
我刚试了一下,它按照预期工作。而且我正在代码中滚动 :) - Bojan Kseneman
显示剩余9条评论
5个回答

20

尝试增加池中可回收视图的最大数量:

recyclerView.getRecycledViewPool().setMaxRecycledViews(50);

50是一个任意的数字,你可以尝试使用更高或更低的数字并观察结果。

RecyclerView试图避免重用具有暂时状态的视图,因此如果视图快速失效或正在进行动画,则可能不能立即重新使用这些视图。

同样地,如果您有许多较小的视图,则可能会在屏幕上显示比默认池大小更多的视图(在类似网格的布局中更为常见)。


当我使用5、10、15、30、50、100、150和1000等数字尝试时,没有注意到立即的变化。我会对瞬态状态进行一些研究,并回来处理这个问题。非常感谢您的解释。我开始看到了一些契机。 :) - Dreamingwhale
1
这对我在实现快速滚动器时非常有帮助。它使用scrollToPosition,并且如果不进行池大小调整,每次设置新位置时RecyclerView会让我为所有项目膨胀布局。不用说,这对性能来说是不好的。 - Malcolm
非常感谢!这解决了快速滚动时的性能问题!! - Kirill Vashilo
这对我很有用,谢谢! - peng gao

3
毕加索是你的支撑,但建议自己构建机制不是正确的方法。
在过去的一年中,毕加索已经落后了,现在有更好的替代品,如Google GlideFacebook Fresco,它们专门发布了更新以更好地与RecyclerView配合使用,并在许多测试中证明了在加载、缓存和存储方面更快、更高效,例如: 希望这可以帮助你。 祝你好运。

是的,图像库和所有内容都很有意义。但即使没有图像,这种情况也会发生。我已经尝试过UIL、Picasso、Glide,目前正在使用Glide。更根本的问题是我的recyclerview为那些不在屏幕上的视图创建新的viewholder。这是因为我使用了focusables进行导航。这里真正致命的是充气所需的时间。 - Dreamingwhale
谢谢。我知道Glide,但是我没有得到任何使用它替代Picasso的动力。现在我已经被说服了,因为我正在密集地使用RecyclerView。 - Ralphilius
证明了更快在哪里? - Daniel Gomez Rico

2

正如评论者所指出的,Picasso可能会拖慢你的响应速度。如果是这种情况,你可以通过扩展ImageView并重写以下方法来解决问题。我认为值得一试。

@Override
protected void onDetachedFromWindow() {
    Picasso.with(context).cancelRequest(this);
    super.onDetachedFromWindow();
}

更新:

结果证明这不是正确的方法,任何想要取消请求的人都应该在下面评论中指出的onViewRecycled()回调中这样做。


谢谢,我对这个有很高的期望,但它似乎不起作用。我尝试了1000个元素,并通过按住键滚动它们,最终创建了330~370,无论我是否使用这段代码。 - Dreamingwhale
1
这是错误的,一个View可能会被RecyclerView分离并缓存。 相反,当RecyclerView调用Adapter的onVIewRecycled回调时,您可以调用Picasso取消。 - yigit
@yigit 謝謝。但我不認為圖像加載是問題所在。我可以刪除圖像加載調用,它仍然會創建200張新卡片(即使只需要約10張)。 - Dreamingwhale
是的,我也不认为这是问题,我只是想澄清一下,在分离时无法取消加载。 - yigit

1
通过深入研究Sam Judd的回答,我在其适配器中实现了以下内容,强制回收站回收视图。
@Override
public boolean onFailedToRecycleView(@NonNull VH holder) 
      return true;
}

如您所见 here
如果由于其短暂状态而无法回收此Adapter创建的ViewHolder,则由RecyclerView调用。在接收到此回调时,Adapter可以清除影响View短暂状态的动画,并返回true,以便可以回收View。请记住,受影响的View已从RecyclerView中删除。在某些情况下,即使具有短暂状态也可以回收View。大多数情况下,当View绑定到新位置时,在onBindViewHolder(ViewHolder,int)调用中会清除短暂状态。出于这个原因,RecyclerView将决策留给Adapter,并使用此方法的返回值来决定是否应该回收View。请注意,当所有动画都由RecyclerView.ItemAnimator创建时,您永远不应该收到此回调,因为RecyclerView将这些View作为子项保留,直到它们的动画完成。当item视图的子项创建难以使用RecyclerView.ItemAnimator实现的动画时,此回调非常有用。您永远不应该通过调用holder.itemView.setHasTransientState(false)来解决此问题,除非您先前调用了holder.itemView.setHasTransientState(true)。每个View.setHasTransientState(true)调用必须与一个View.setHasTransientState(false)调用匹配,否则,View的状态可能变得不一致。您应始终优先结束或取消触发短暂状态的动画,而不是手动处理它。

-2

对于其他寻找快捷方法的人, 这样做。 这将延迟选择,直到它被充气和选中。 我不知道为什么,但它能够起作用。 只需在onFocusSearchFailed上返回null即可。

 /**
 * Created by sylversphere on 15-04-22.
 */
public class SomeGridLayoutManager extends GridLayoutManager{

    private final Context context;

    public SomeGridLayoutManager(Context context, int spanCount) {
        super(context, spanCount);
        this.context = context;
    }

    public SomeGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
        this.context = context;
    }

    @Override
    public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) {
        return null;
    }
}

也试试这两个。 lm.setExtraLayoutSpace(height); recyclerView.setItemViewCacheSize(numViews); - Dreamingwhale

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接