从 leanback 的npe崩溃谈起
最近接手了一个TV的应用,然后看到有些历史的崩溃。
其中top3有这样一个崩溃,但是被离职的前同事标记为不处理了。
崩溃是这样的:
java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.ViewGroup$LayoutParams android.view.View.getLayoutParams()' on a null object reference
at androidx.recyclerview.widget.OrientationHelper$2.getDecoratedStart(Proguard:403)
at androidx.leanback.widget.GridLayoutManager.getViewMin
at androidx.leanback.widget.GridLayoutManager$2.getEdge(Proguard:1812)
at androidx.leanback.widget.Grid.removeInvisibleItemsAtEnd
at androidx.leanback.widget.GridLayoutManager.removeInvisibleViewsAtEnd(Proguard:1906)
at androidx.leanback.widget.GridLayoutManager.scrollDirectionPrimary(Proguard:2545)
at androidx.leanback.widget.GridLayoutManager.scrollVerticallyBy(Proguard:2487)
at androidx.recyclerview.widget.RecyclerView.scrollStep(Proguard:1974)
at androidx.recyclerview.widget.RecyclerView$ViewFlinger.run(Proguard:5476)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:727)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7661)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
issuetracker
issuetracker上显示已经修复了,更新日志中也可以看到这个的信息
https://issuetracker.google.com/issues/177338150
https://android-review.googlesource.com/c/platform/frameworks/support/+/2808353
https://developer.android.com/jetpack/androidx/releases/leanback?hl=zh-cn
https://developer.android.com/jetpack/androidx/releases/leanback?hl=zh-cn#leanback-grid_version_100_2
Version 1.2.0-alpha04
November 15, 2023androidx.leanback:leanback:1.2.0-alpha04 and androidx.leanback:leanback-preference:1.2.0-alpha04 are released. Version 1.2.0-alpha04 contains these commits.Bug Fixes(I2c3a0, b/292114537)
https://android-review.googlesource.com/#/q/I2c3a0f7ae43f72bd6a1dbbe30c269148f824a885
https://issuetracker.google.com/issues/292114537
Dependency UpdateUpdate recyclerview requirement to 1.3.2 to fix a common crash in TV apps
修复方法
dependencies {
def leanback_version = "1.2.0"
implementation "androidx.leanback:leanback:$leanback_version"
implementation "androidx.leanback:leanback-tab:1.1.0"
}
注意到这个升级了recyclerview到1.3.2之后。
compile_sdk_version 也需要升级到35,即 compileSdk 35
1.2.0 leanback ui 紊乱
本来上个小节之后,这篇博客应该就结束了。
但是我自测发现升级之后,列表刷新之后,出现了ui紊乱,即列表叠在一起。
项目中是如何做列表刷新的?
在 Android TV 开发中,Leanback
库的 DiffCallback
用于高效比较数据集合并更新 ArrayObjectAdapter
的内容,通常结合 setItems(List, DiffCallback)
方法使用。以下是其核心用法:
1. 作用
- 高效更新列表:通过比较新旧数据集差异,仅更新变化的项(支持动画效果)。
- 避免全量刷新:减少不必要的 UI 重绘,提升性能。
2. 实现步骤
a. 创建 DiffCallback 子类
继承 DiffCallback
并实现两个关键方法:
public class MyDiffCallback extends DiffCallback<YourItemType> {@Override
public boolean areItemsTheSame(YourItemType oldItem, YourItemType newItem) {
// 判断是否为同一个项(如唯一ID相同)
return oldItem.getId() == newItem.getId();
}@Override
public boolean areContentsTheSame(YourItemType oldItem, YourItemType newItem) {
// 判断内容是否相同(如字段值完全一致)
return oldItem.equals(newItem);
}@Optional @Override
public Object getChangePayload(YourItemType oldItem, YourItemType newItem) {
// 可选:返回变化的部分数据,用于局部更新
return super.getChangePayload(oldItem, newItem);
}
}
b. 使用 DiffCallback 更新数据
通过 ArrayObjectAdapter
的 setItems()
方法触发差异计算:
ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenter);
List<YourItemType> newItems = ...; // 新数据集// 更新数据并自动应用差异
adapter.setItems(newItems, new MyDiffCallback());
3. 示例场景
假设有一个 Movie
类作为数据项:
public class Movie {
private int id;
private String title;
private String description;// Getters & Setters...
}public class MovieDiffCallback extends DiffCallback<Movie> {
@Override
public boolean areItemsTheSame(Movie oldItem, Movie newItem) {
return oldItem.getId() == newItem.getId(); // ID 相同则为同一项
}@Override
public boolean areContentsTheSame(Movie oldItem, Movie newItem) {
return oldItem.getTitle().equals(newItem.getTitle())
&& oldItem.getDescription().equals(newItem.getDescription()); // 内容完全一致
}
}
4. 注意事项
- 正确实现比较逻辑:
areItemsTheSame
应基于唯一标识符(如 ID),areContentsTheSame
判断数据是否变化。 - 性能优化:避免在
areContentsTheSame
中进行耗时操作。 - 支持局部更新:通过
getChangePayload
返回变化的部分数据,在 Presenter 中处理onBindViewHolder
的局部更新。
UI错乱原因
项目中的 ConstraintLayout 中的view有点复杂,特别是在计算高度的时候,都是动态计算的。
在动画过程中,计算出来的结果出现了变化,所以导致了最终的效果是错乱的。
修复UI错乱的方法
-
和iOS一样,去掉动画,即 DiffCallback 传空进去。
-
重写 getChangePayload 和 public void onBindViewHolder(@NonNull ViewHolder viewHolder,@NonNull Object item,@NonNull List payloads) payloads变化的时候忽略 onBindViewHolder,然后定时400ms post一下,刷新列表。
-
固定View的高度、并且把高度依赖计算的view替换成 LinearLayout(或者其他容器)的子view