问题记录-RecyclerView加载上千条数据的时候ANR

       项目里有个普通的详情页面,有时候跳转到该页面后直接卡死,手指点几下屏幕后,就出现ANR,或者是容推送通知里跳转该详情页面,会出现黑屏现象。想了想,虽然我的列表数据没有分页,有些详情的item数据会有一千多条,但item绘制超出一屏后item就会被复用的,理论上没问题。我用命令adb shell data/anr导出整个anr文件夹,对应产生anr的时间,打开traces.txt文件,发现我们的adapter里onCreateViewHolder()方法,意思是该方法被频繁调用。检查了下我们的布局文件,发现在RecyclerView的外层竟然嵌套了一层NestScrollView,加了他以后,他会把子view里数据全部绘制,然后一起加载,这就导致了onCreateViewHolder()被调用多次,去掉外层的ScrollView,问题解决。

package com.example.coursemanager.activities; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.example.coursemanager.R; import com.example.coursemanager.models.Course; import com.example.coursemanager.services.CourseReminderService; import com.example.coursemanager.utils.CourseDbHelper; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.List; public class MainActivity extends AppCompatActivity { private static final int ADD_COURSE_REQUEST = 1; private static final int REQUEST_CALENDAR_VIEW = 2; private CourseDbHelper dbHelper; private ListView listView; private ArrayAdapter<Course> adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startReminderService(); // 初始化数据库 dbHelper = new CourseDbHelper(this); // 初始化Material工具栏 MaterialToolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); // 初始化ListView和自定义Adapter listView = findViewById(R.id.course_list); adapter = new ArrayAdapter<Course>(this, R.layout.item_course, R.id.course_name) { @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view = super.getView(position, convertView, parent); Course course = getItem(position); // 绑定视图数据 TextView timeView = view.findViewById(R.id.course_time); TextView teacherView = view.findViewById(R.id.course_teacher); timeView.setText(formatCourseTime(course));// 设置课程时间 teacherView.setText(course.getTeacher());// 设置教师名称 return view; } // 格式化课程时间显示 private String formatCourseTime(Course course) { return String.format("%s %02d:%02d-%02d:%02d", getDayName(course.getDay()), course.getStartTime() / 60,// 开始小时 course.getStartTime() % 60,// 开始分钟 course.getEndTime() / 60, course.getEndTime() % 60); } // 获取星期名称 private String getDayName(int day) { String[] days = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; return (day >= 1 && day <= 7) ? days[day - 1] : "未知"; } }; listView.setAdapter(adapter);// 设置适配器 // 设置悬浮按钮点击事件 FloatingActionButton addButton = findViewById(R.id.add_button); addButton.setOnClickListener(v -> { Intent intent = new Intent(MainActivity.this, AddCourseActivity.class); startActivityForResult(intent, ADD_COURSE_REQUEST); }); // 设置列表项点击事件 listView.setOnItemClickListener((parent, view, position, id) -> { Course course = (Course) parent.getItemAtPosition(position); Intent intent = new Intent(MainActivity.this, CourseDetailActivity.class); intent.putExtra("course_id", course.getId()); startActivity(intent); }); // 首次加载数据 refreshCourseList(); } // 刷新课程列表 private void refreshCourseList() { List<Course> courses = dbHelper.getAllCourses();// 从数据库获取所有课程 Log.d("MainActivity", "加载到 " + courses.size() + " 课程数据"); adapter.clear(); // 清空适配器 adapter.addAll(courses);// 添加新数据 } @Override public boolean onCreateOptionsMenu(Menu menu) { // 加载菜单布局 getMenuInflater().inflate(R.menu.main_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_calendar) { // 切换到日历视图 Intent intent = new Intent(this, CalendarActivity.class); startActivityForResult(intent, REQUEST_CALENDAR_VIEW); return true; } else if (id == R.id.action_settings) { // 设置按钮点击事件 Toast.makeText(this, "设置功能开发中", Toast.LENGTH_SHORT).show(); return true; } return super.onOptionsItemSelected(item); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); // 处理从添加课程界面返回的结果 if (requestCode == ADD_COURSE_REQUEST && resultCode == RESULT_OK) { refreshCourseList(); } // 从日历视图返回时也刷新数据 else if (requestCode == REQUEST_CALENDAR_VIEW && resultCode == RESULT_OK) { refreshCourseList(); } } private void startReminderService() { Intent serviceIntent = new Intent(this, CourseReminderService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(serviceIntent); } else { startService(serviceIntent); } } @Override protected void onResume() { super.onResume(); refreshCourseList();// 每次回到界面时刷新数据 } @Override protected void onDestroy() { dbHelper.close(); super.onDestroy(); } }这是MainActivity的代码
06-09
<think>我们分析一下这个MainActivity的代码。这是一个课程管理应用的主活动(MainActivity),它主要包含以下功能:1.初始化数据库(CourseDbHelper)2.设置工具栏(MaterialToolbar)3.使用ListView展示课程列表,并自定义了一个ArrayAdapter来显示课程的名称、时间和教师。4.有一个悬浮按钮(FloatingActionButton)用于跳转到添加课程界面(AddCourseActivity)5.列表项点击事件,点击后跳转到课程详情(CourseDetailActivity)6.菜单选项:日历视图(跳转到CalendarActivity)和设置(暂时未实现)7.在onResume时刷新列表,保证每次回到界面数据都是最新的。8.启动一个提醒服务(CourseReminderService)9.处理从其他活动返回的结果,当添加课程或从日历视图返回时刷新列表。代码中还有一些辅助方法,如格式化课程时间等。需要指出的是,这里使用了自定义的Adapter来显示ListView的每一项。自定义的布局在R.layout.item_course中,里面包含三个TextView:课程名称、课程时间和教师名称。代码中有一个问题:在自定义ArrayAdapter中,只重写了getView方法,但是ArrayAdapter的构造中传入了一个布局资源(R.layout.item_course)和一个TextView的id(R.id.course_name)。ArrayAdapter默认的行为是使用这个布局,并将这个TextView作为主要的显示控件,然后调用setText方法设置文本。然而,在getView方法中,我们调用了super.getView方法,该方法已经将课程对象(Course)的字符串表示设置到了R.id.course_name这个TextView上(因为ArrayAdapter默认使用对象的toString方法)。但是,在代码中,我们并没有重写Course类的toString方法。所以,默认会显示Course对象的类名和内存地址,这显然不是我们想要的。因此,我们需要在Course类中重写toString方法,返回课程名称(或者你想要显示的名称)。或者,我们也可以在ArrayAdapter中通过覆盖getView方法来自行设置课程名称,而不是依赖父类的方法。在当前的代码中,其实已经做了自定义:在getView方法中,我们除了调用super.getView(它会设置course_name的文本为Course.toString())之外,还设置了时间和教师。所以,如果我们不在Course类中重写toString,那么显示在course_name上的文本就会是默认的(如com.example.coursemanager.models.Course@xxxx)。解决方案:在Course类中重写toString方法,返回课程名称(courseName)或者你想要显示的名称。例如:```java@OverridepublicStringtoString(){returnthis.courseName;//假设Course类中有一个courseName字段}```另外,还有一种做法:不在ArrayAdapter的构造中指定一个TextView的id,而是完全自己控制整个布局。可以将自定义的Adapter改为继承BaseAdapter,或者使用SimpleAdapter,也可以继承ArrayAdapter但重写getView方法并自己处理所有的视图(包括课程名称)。当前代码中,在getView方法中通过`super.getView(position,convertView,parent)`获取了View,然后在这个View的基础上再设置时间和教师。所以,如果我们在Course类中重写toString,就可以让课程名称正确显示。但是,我们注意到ArrayAdapter的构造中使用了三个参数:this,R.layout.item_course,R.id.course_name这个构造方法的意思是:使用item_course布局,每个项中的哪个TextView作为TextViewResourceId(即用于显示文本的TextView,ArrayAdapter会在这里调用setText(toString()))。因此,我们有两种方式:1.在Course类中重写toString,返回课程名称。这样父类的方法就会将课程名称设置到R.id.course_name这个TextView上。2.不在父类方法中设置课程名称,而是自己在getView方法中设置课程名称(即不调用super.getView,或者调用super.getView之后,再设置课程名称,这样也可以覆盖父类设置的值)。但是当前代码中没有对课程名称的TextView设置值。观察代码,我们并没有看到在getView方法中为课程名称的TextView设置值。所以,我们必须保证父类(ArrayAdapter)能够正确设置课程名称,也就是必须重写Course的toString方法返回课程名称。因此,在Course类中添加toString方法返回课程名称是必要的。除了上述问题,代码逻辑上还可能有其他需要注意的地方,比如:-启动服务的方式:在Android8.0及以上,必须使用startForegroundService,否则会报错。但是注意在Service中必须调用startForeground来使其成为前台服务,否则会报ANR。下面,我提供在Course类中添加toString方法的示例:假设Course类有一个字段叫做courseName(或类似名称),用于存储课程名。修改Course类:```javapublicclassCourse{//其他字段...privateStringcourseName;//课程名称//构造方法、getter、setter等...@OverridepublicStringtoString(){returncourseName;}}```这样,ArrayAdapter就会将课程名称设置为R.id.course_name的文本。此外,代码中有一个日志记录:Log.d("MainActivity","加载到"+courses.size()+"课程数据");这有助于调试。最后,关于提醒服务(CourseReminderService)的启动,在onCreate中调用startReminderService方法。该方法会根据系统版本启动服务。但注意,在Android8.0及以上,如果服务被启动后,没有在5秒内调用startForeground,会导致ANR。所以请确保在CourseReminderService中已经正确处理了前台服务的要求。如果CourseReminderService没有实现为前台服务(即没有在onCreate或onStartCommand中调用startForeground),那么在Android8.0以上版本会导致崩溃。因此,我们需要检查CourseReminderService的代码,确保其正确使用startForeground。综上所述,MainActivity的主要改进点:1.确保Course类重写了toString方法返回课程名称。2.确保CourseReminderService在Android8.0以上设备上能够正确使用startForeground成为前台服务。如果没有其他问题,这个Activity应该是可以正常工作的。现在,针对上述分析,提出相关问题:§§相关问题§§:1.如何为ListView实现自定义Adapter而不使用ArrayAdapter?2.在Android8.0及以上如何正确启动前台服务?3.在适配器中如何优化视图的加载(使用ViewHolder模式)?4.如何对课程时间进行更友好的格式化(例如:将分钟数转为时间格式)?</think> 以下是对 `MainActivity` 代码的分析和改进建议,主要针对代码结构、性能和功能完善性方面: ### 代码分析及优化建议 #### 1. 适配器效率问题 **问题**:直接使用 `ArrayAdapter` 的 `clear()` 和 `addAll()` 刷新数据导致视图完全重绘 **解决方案**:使用自定义 BaseAdapter 或 RecyclerView.Adapter ```java // 替代方案:使用 RecyclerView + 自定义适配器 (需要修改布局) public class CourseAdapter extends RecyclerView.Adapter<CourseAdapter.ViewHolder> { private List<Course> courses = new ArrayList<>(); public void updateData(List<Course> newCourses) { // 使用 DiffUtil 实现高效增量更新 DiffUtil.DiffResult result = DiffUtil.calculateDiff(new CourseDiff(courses, newCourses)); courses.clear(); courses.addAll(newCourses); result.dispatchUpdatesTo(this); } static class ViewHolder extends RecyclerView.ViewHolder { TextView nameView, timeView, teacherView; public ViewHolder(View view) { super(view); nameView = view.findViewById(R.id.course_name); timeView = view.findViewById(R.id.course_time); teacherView = view.findViewById(R.id.course_teacher); } } } // MainActivity 中 RecyclerView recyclerView = findViewById(R.id.recycler_view); CourseAdapter adapter = new CourseAdapter(); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(adapter); ``` #### 2. 数据库操作优化 **问题**:在主线程执行数据库查询 **解决方案**:使用 AsyncTask 或 RxJava ```java private void refreshCourseList() { new AsyncTask<Void, Void, List<Course>>() { @Override protected List<Course> doInBackground(Void... voids) { return dbHelper.getAllCourses(); } @Override protected void onPostExecute(List<Course> courses) { adapter.updateData(courses); } }.execute(); } ``` #### 3. 服务启动逻辑改进 **问题**:每次 onCreate 都启动服务 **解决方案**:添加服务检查 ```java private void startReminderService() { if (!isServiceRunning(CourseReminderService.class)) { Intent serviceIntent = new Intent(this, CourseReminderService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(serviceIntent); } else { startService(serviceIntent); } } } private boolean isServiceRunning(Class<?> serviceClass) { ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { if (serviceClass.getName().equals(service.service.getClassName())) { return true; } } return false; } ``` #### 4. 时间格式化工具 **问题**:格式化逻辑在适配器内部 **解决方案**:提取为静态工具类 ```java // 新增 DateUtils.java public class DateUtils { public static String formatCourseTime(Course course) { // 实现相同的格式化逻辑 } public static String getDayName(int day) { // 实现相同的获取星期名逻辑 } } // 在适配器中使用 timeView.setText(DateUtils.formatCourseTime(course)); ``` ### 代码改进亮点说明 1. **RecyclerView替代ListView**: - 支持增量更新(DiffUtil) - 自带ViewHolder模式减少findViewById调用 - 更灵活的布局管理 2. **异步数据库操作**: - 避免主线程阻塞 - 防止ANR(应用无响应)错误 3. **服务管理优化**: - 防止重复启动服务 - 减少不必要的系统资源占用 4. **工具类封装**: - 提高代码复用性 - 符合单一职责原则 5. **数据更新机制**: - DiffUtil精准计算数据变化 - 最小化视图更新操作
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值