Xamarin Android滚动图片ListView时触发System.ObjectDisposedException
问题描述
使用Xamarin.Forms开发Android应用时,包含图片列表的页面抛出 System.ObjectDisposedException: 'Cannot access a closed Stream.' 异常,触发规律为:向下滚动ListView时图片加载正常,向上回滚列表时触发异常。
页面所用图片以byte[]格式存储在数据库中,当前实现通过MemoryStream为Image控件的Source属性赋值,已确认异常由流被关闭导致,无法找到维持流打开状态或规避问题的可行方案。
原实现代码
页面XAML代码
<ListView ItemsSource="{Binding imageGallery}" x:Name="imageList" RowHeight="100" VerticalScrollBarVisibility="Always"> <ListView.ItemTemplate > <DataTemplate> <ViewCell> <Grid > <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*" /> <ColumnDefinition Width="0.5*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="100"/> </Grid.RowDefinitions> <Image Source="{Binding photo.Source}" x:Name="imageInGrid" Rotation="90" Grid.Column="0" MinimumHeightRequest="100" VerticalOptions="FillAndExpand"/> <StackLayout Orientation="Vertical" Grid.Column="1" VerticalOptions="Center" > <Label Text="{Binding date}" FontSize="18" TextColor="Navy" HorizontalTextAlignment="Start" VerticalTextAlignment="Center" /> <Label Text="{Binding manifestNum}" FontSize="18" TextColor="Navy" HorizontalTextAlignment="Start" VerticalTextAlignment="Center" FontAttributes="Bold" /> </StackLayout> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView>
页面C#代码
static ObservableCollection<ImageViewer> imageGallery; static List<Stream> streams; public ImagesPage() { InitializeComponent(); imageGallery = new ObservableCollection<ImageViewer>(); streams = new List<Stream>(); //query string using (SqlConnection conn = new SqlConnection(queryString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(query, conn)) { using (SqlDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) { ImageViewer viewer = new ImageViewer { imageData = (byte[]) reader[2], photo = new Image(), date = reader[1].ToString().Split(' ')[0], manifestNum = reader[0].ToString().Substring(4) }; Stream stream = new MemoryStream(viewer.imageData); viewer.photo.Source = ImageSource.FromStream(() => { return stream; }); streams.Add(stream); imageGallery.Add(viewer); } } } } imageList.ItemsSource = imageGallery; }
问题根因
- ListView自带视图回收复用机制:滚动时离屏的列表项会被缓存,回滚到对应位置时会重新触发图片加载流程,调用
ImageSource.FromStream传入的工厂委托获取流 - 原代码只在列表初始化时为每张图创建了一次MemoryStream实例,图片首次加载完成后,Xamarin.Forms的平台渲染层会自动关闭并释放这个流。回滚复用时再次访问已释放的流,就会抛出ObjectDisposedException
- 原代码存在冗余设计:在数据模型层提前创建Image UI控件完全没有必要,DataTemplate会自动生成对应UI元素,提前创建只会额外占用内存
- 把集合、流列表设为static会导致页面销毁后资源无法被GC回收,引发内存泄漏
修复方案
核心修复逻辑:不要缓存复用同一个MemoryStream实例,每次FromStream的工厂委托被调用时,基于存储的byte[]新建MemoryStream即可。平台加载完图片后会自动释放这个新建的流,下次回滚复用时重新生成新流,就不会出现访问已释放流的问题。
修正后C#代码
// 移除static修饰和无意义的流缓存列表,避免内存泄漏 ObservableCollection<ImageViewer> imageGallery; public ImagesPage() { InitializeComponent(); imageGallery = new ObservableCollection<ImageViewer>(); using (SqlConnection conn = new SqlConnection(queryString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(query, conn)) { using (SqlDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) { byte[] imgBytes = (byte[])reader[2]; ImageViewer viewer = new ImageViewer { imageData = imgBytes, date = reader[1].ToString().Split(' ')[0], manifestNum = reader[0].ToString().Substring(4), // 关键:每次委托调用都新建MemoryStream,不复用旧流 PhotoSource = ImageSource.FromStream(() => new MemoryStream(imgBytes)) }; imageGallery.Add(viewer); } } } } imageList.ItemsSource = imageGallery; } // 修正数据模型,移除冗余的UI控件字段,只保留绑定需要的数据 public class ImageViewer { public byte[] imageData { get; set; } public string date { get; set; } public string manifestNum { get; set; } public ImageSource PhotoSource { get; set; } }
修正后XAML代码
<!-- 可添加CachingStrategy="RecycleElement"进一步优化列表滚动性能 --> <ListView ItemsSource="{Binding imageGallery}" x:Name="imageList" RowHeight="100" VerticalScrollBarVisibility="Always" CachingStrategy="RecycleElement"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid > <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*" /> <ColumnDefinition Width="0.5*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="100"/> </Grid.RowDefinitions> <!-- 绑定路径改为数据模型中的PhotoSource属性 --> <Image Source="{Binding PhotoSource}" x:Name="imageInGrid" Rotation="90" Grid.Column="0" MinimumHeightRequest="100" VerticalOptions="FillAndExpand"/> <StackLayout Orientation="Vertical" Grid.Column="1" VerticalOptions="Center" > <Label Text="{Binding date}" FontSize="18" TextColor="Navy" HorizontalTextAlignment="Start" VerticalTextAlignment="Center" /> <Label Text="{Binding manifestNum}" FontSize="18" TextColor="Navy" HorizontalTextAlignment="Start" VerticalTextAlignment="Center" FontAttributes="Bold" /> </StackLayout> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView>
优化说明
- 不需要手动维护流的生命周期,每次工厂委托中新建的MemoryStream会在图片加载完成后被平台自动释放,不会造成内存泄漏
- 开启
RecycleElement缓存策略后,列表滚动时只会复用离屏的Cell对象,不需要反复创建销毁UI元素,能大幅降低内存占用、提升滚动流畅度 - 数据模型层不要存放UI控件实例,遵循数据和UI分离的原则,避免跨线程访问UI、内存泄漏等问题
内容的提问来源于stack exchange,提问作者Kristopher Agarwal
相关产品推荐
相关产品推荐

