调用notifyDataSetChanged后BillingClient回调阻塞,RecyclerView无显示
问题分析与解决方案
核心原因1:回调线程非主线程
Google Play Billing Library 4.x+版本中,queryProductDetailsAsync的回调运行在后台线程,而notifyDataSetChanged()属于UI操作,必须在主线程执行。直接在回调中调用会导致UI更新被延迟,直到RecyclerView触发重绘(比如滚动),甚至引发线程阻塞。
解决方案:切换到主线程更新UI
通过Activity.runOnUiThread将UI更新操作切到主线程:
billingClient.queryProductDetailsAsync( params, new ProductDetailsResponseListener() { public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetailsList) { // 切换至主线程执行UI更新逻辑 getActivity().runOnUiThread(() -> { inventoryAsyncSub.addAll(productDetailsList); subscriptionAdapter.notifyDataSetChanged(); }); } } );
核心原因2:列表线程不安全
如果inventoryAsyncSub是普通ArrayList,后台线程执行addAll时,主线程的Adapter同时读取会引发并发修改问题,导致数据不同步,RecyclerView无法正常渲染。
解决方案:保证线程安全
- 推荐方案:将数据添加操作和UI更新一起移到主线程执行(数据与UI更新逻辑统一,避免并发问题)
- 备选方案:使用
CopyOnWriteArrayList作为inventoryAsyncSub的实现类,天生支持线程安全的读写操作
核心原因3:静态ClickListener引发内存泄漏
Adapter中的static ClickListener clickListener持有Activity/Fragment引用,Fragment退出后,静态引用无法被GC回收,导致Activity实例无法释放,进而引发应用卡住、无响应。
解决方案:移除静态引用,改用非静态监听
修改Adapter的监听实现:
// 移除静态变量 private ClickListener clickListener; private Context context; private List<ProductDetails> inventoryAsyncSub; // 构造方法调整,直接传入监听,移除Activity参数 public SubscriptionAdapter( Context context, List<ProductDetails> inventoryAsyncSub, ClickListener clickListener ) { this.context = context; this.inventoryAsyncSub = inventoryAsyncSub; this.clickListener = clickListener; } // ViewHolder中的点击逻辑 @Override public void onClick(View v) { if (clickListener != null) { clickListener.onItemClick(getAdapterPosition(), v); } } // 移除原有setOnItemClickListener方法
Fragment中创建Adapter时直接传入监听:
SubscriptionAdapter subscriptionAdapter = new SubscriptionAdapter( getContext(), inventoryAsyncSub, (position, view) -> { // 处理订阅项点击逻辑 } );
额外优化点
- ViewHolder性能优化:将
findViewById移到ViewHolder构造方法中,避免每次绑定视图重复查找:
public Viewholder(@NonNull View itemView) { super(itemView); name = itemView.findViewById(R.id.submition_name); description = itemView.findViewById(R.id.submition_description); pricesBox = itemView.findViewById(R.id.purchase_btn); // 提前初始化price控件 price = pricesBox.findViewById(R.id.item_price); itemView.setOnClickListener(this); } @Override public void onBindViewHolder(@NonNull SubscriptionAdapter.Viewholder holder, int position) { ProductDetails subScr = inventoryAsyncSub.get(position); PurchaseHandlerTools.setItemTitle((Activity) context, holder.name, subScr.getTitle()); holder.description.setText(subScr.getDescription()); // 增加空指针判断,避免subscriptionOfferDetails为空时崩溃 if (!subScr.getSubscriptionOfferDetails().isEmpty()) { holder.price.setText(subScr.getSubscriptionOfferDetails().get(0) .getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()); } }
- 减少不必要的引用持有:Adapter只需持有Context即可,无需持有Activity实例,降低内存泄漏风险。
内容的提问来源于stack exchange,提问作者Enrico Ancis
相关产品推荐
相关产品推荐

