我构建了整个应用程序,认为垃圾收集器处理内存清理就好了,这对我来说非常愚蠢和天真,但是,嘿,这是我第一次使用Xamarin构建应用程序,而我的第一次是时候建立一个应用程序了,那么一个人要做什么?每个屏幕似乎泄漏内存,但泄漏最多的屏幕是具有位图的屏幕,生成内存转储并在MAT中进行分析,我发现以下内容:
因此有4个潜在的罪魁祸首,2个是位图,2个是字节数组。这是应用程序主菜单的堆转储,如果我进入列表视图活动列出元素,我会从位图中获得5个潜在泄漏。以下是活动的代码:
AssetManager assets = Assets;
Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
var topPanel = FindViewById<TextView>(Resource.Id.topPanel);
topPanel.Text = service.GetLanguageValue("use recommendations - top bar heading");
topPanel.Dispose();
var lowerPanel = FindViewById<TextView>(Resource.Id.recommendationsPanel);
lowerPanel.Text = service.GetLanguageValue("title upper - recommendations by variety");
Shared.ScaleTextToOneLine(lowerPanel, lowerPanel.Text, Shared.ScaleFloatToDensityPixels(Shared.GetViewportWidthInDp()), 1.0f);
lowerPanel.Dispose();
// Read html file and replace it's contents with apple data
string html = "";
using (StreamReader sr = new StreamReader(Assets.Open("apple-variety-detail.html")))
{
html = sr.ReadToEnd();
}
html = ReplaceAppleDetailsHtml(html);
var webview = FindViewById<WebView>(Resource.Id.recommendationsMessage);
CleanWebView();
webview.LoadDataWithBaseURL("file:///android_asset/",
html,
"text/html", "UTF-8", null);
if (Shared.currentApple != null)
{
// Setup apple image
using (var imageView = FindViewById<ImageView>(Resource.Id.recommendationsImage))
{
var apple = this.apples.Where(a => a.Id == Shared.currentApple.AppleId).Select(a => a).First();
var imgName = apple.Identifier.First().ToString().ToUpper() + apple.Identifier.Substring(1);
var fullImageName = "SF_" + imgName;
using (var bitmap = Shared.decodeSampledBitmapFromResource(ApplicationContext.Resources,
Resources.GetIdentifier(fullImageName.ToLower(), "drawable", PackageName),
200, 200))
{
imageView.SetImageBitmap(bitmap);
}
}
// Setup apple name
FindViewById<TextView>(Resource.Id.appleNameTextView).Text = Shared.currentApple.Name;
}
else
{
FindViewById<TextView>(Resource.Id.appleNameTextView).Text = "Not Found!";
}
// Setup list menu for apples
AppleListView = FindViewById<ListView>(Resource.Id.ApplesListMenu);
// Scale details and list to fit on the same screen if the screen size permits
if (Shared.GetViewportWidthInDp() >= Shared.minPhoneLandscapeWidth)
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(240);
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
else
{
// Here, we either need to hide the list view if an apple was selected,
// or set it to be 100% of the screen if it wasn't selected.
if(!Shared.appleSelected)
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(Shared.GetViewportWidthInDp());
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
else
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(0);
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
}
// Set listview adapter
if(AppleListView.Adapter == null)
{
AppleListView.Adapter = new Adapters.AppleListAdapter(this, (List<Apple>)apples, this);
}
AppleListView.FastScrollEnabled = true;
// Set the currently active view for the slide menu
var frag = (SlideMenuFragment)FragmentManager.FindFragmentById<SlideMenuFragment>(Resource.Id.SlideMenuFragment);
frag.SetSelectedLink(FindViewById<TextView>(Resource.Id.SlideMenuRecommendations));
// Replace fonts for entire view
Typeface tf = Typeface.CreateFromAsset(assets, "fonts/MuseoSansRounded-300.otf");
FontCrawler fc = new FontCrawler(tf);
fc.replaceFonts((ViewGroup)this.FindViewById(Android.Resource.Id.recommendationsRootLayout));
tf.Dispose();
}
要注意的重要部分是此活动的工作方式是加载适配器,当它显示它显示项目列表时,单击项目时,它会重新加载此相同的活动,并计算屏幕大小,缩小列表以仅显示侧面的webview,并显示有关项目的详细信息,从而模拟2个屏幕,我这样做的原因是因为当屏幕尺寸较大时,它需要显示所有这些一个单一的视图,所以在较大的屏幕上它实际上会显示listview和webview,但仍然重新加载活动以加载新数据。
适配器代码可能是给我带来困难的,但我不确定,我已经尝试了很多东西,但似乎没有任何帮助,这里是适配器代码:
public class AppleListAdapter : BaseAdapter<Apple>
{
List<Apple> items;
Activity context;
ApplicationService service = AgroFreshApp.Current.ApplicationService;
private Context appContext;
private Typeface tf;
static AppleRowViewHolder holder = null;
public AppleListAdapter(Activity context, List<Apple> items, Context appContext): base ()
{
this.context = context;
this.items = items;
this.appContext = appContext;
context.FindViewById<ListView>(Resource.Id.ApplesListMenu).ChoiceMode = ChoiceMode.Single;
tf = Typeface.CreateFromAsset(context.Assets, "fonts/MuseoSansRounded-300.otf");
}
public override long GetItemId(int position)
{
return position;
}
public override Apple this[int position]
{
get { return items[position]; }
}
public override int Count
{
get
{
return items.Count;
}
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = items[position];
var view = convertView;
var imgName = item.Identifier.First().ToString().ToUpper() + item.Identifier.Substring(1);
var fullImageName = "SF_" + imgName;
if (view == null)
{
view = context.LayoutInflater.Inflate(Resource.Layout.appleRowView, null);
}
if (view != null)
{
holder = view.Tag as AppleRowViewHolder;
}
if(holder == null)
{
holder = new AppleRowViewHolder();
view = context.LayoutInflater.Inflate(Resource.Layout.appleRowView, null);
holder.AppleImage = view.FindViewById<ImageView>(Resource.Id.iconImageView);
holder.AppleName = view.FindViewById<TextView>(Resource.Id.nameTextView);
view.Tag = holder;
}
using (var bitmap = Shared.decodeSampledBitmapFromResource(context.Resources,
context.Resources.GetIdentifier(fullImageName.ToLower(), "drawable", context.PackageName),
25, 25))
{
holder.AppleImage.SetImageBitmap(bitmap);
}
holder.AppleName.Text = AgroFreshApp.Current.AppleDetailManager.GetAll().Where(a => a.AppleId == item.Id).Select(a => a.Name).FirstOrDefault();
holder.AppleName.SetTypeface(tf, TypefaceStyle.Normal);
view.Click += (object sender, EventArgs e) =>
{
var apple = AgroFreshApp.Current.AppleManager.Get(item.Id);
Shared.currentApple = AgroFreshApp.Current.AppleDetailManager.GetAll().Where(a=>a.AppleId == item.Id && a.LanguageId == service.UserSettings.LanguageId).Select(a=>a).FirstOrDefault();
Shared.appleSelected = true;
Intent intent = new Intent(appContext, typeof(RecommendationsActivity));
intent.SetFlags(flags: ActivityFlags.NoHistory | ActivityFlags.NewTask);
appContext.StartActivity(intent);
};
return view;
}
}
所以我在这里使用了视图集模式,并在生成它们时为每个列表项分配了click事件,nohistory和newtask作为intent标记,以便页面正确刷新。为了清理位图,我一直在使用这两种方法:
这会清除详细网页视图上的大图片:
public void CleanBitmap()
{
// Clean recommendations bitmap
ImageView imageView = (ImageView)FindViewById(Resource.Id.recommendationsImage);
Drawable drawable = imageView.Drawable;
if (drawable is BitmapDrawable)
{
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
if (bitmapDrawable.Bitmap != null)
{
Bitmap bitmap = bitmapDrawable.Bitmap;
if (!bitmap.IsRecycled)
{
imageView.SetImageBitmap(null);
bitmap.Recycle();
bitmap = null;
}
}
}
Java.Lang.JavaSystem.Gc();
}
这会清除存储在每个listview项目中的位图:
public void CleanListViewBitmaps()
{
var parent = FindViewById<ListView>(Resource.Id.ApplesListMenu);
// Clean listview bitmaps
for (int i = 0; i < parent.ChildCount; i++)
{
var tempView = parent.GetChildAt(i);
// If the tag is null, this no longer holds a reference to the view, so
// just leave it.
if(tempView.Tag != null)
{
AppleRowViewHolder tempHolder = (AppleRowViewHolder)tempView.Tag;
var imageView = tempHolder.AppleImage;
var drawable = imageView.Drawable;
if (drawable is BitmapDrawable)
{
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
if (bitmapDrawable.Bitmap != null)
{
Bitmap bitmap = bitmapDrawable.Bitmap;
if (!bitmap.IsRecycled)
{
imageView.SetImageBitmap(null);
bitmap.Recycle();
bitmap = null;
}
}
}
}
}
Java.Lang.JavaSystem.Gc();
}
然后在活动ondestroy方法中调用它们,如下所示:
protected override void OnDestroy()
{
base.OnDestroy();
CleanBitmap();
CleanListViewBitmaps();
Shared.appleSelected = false;
}
我也使用带有静态变量的共享类来实质上跟踪视图状态,比如是否选择了某些内容,但它只存储基元,它不存储任何视图对象或类似的东西,所以我不喜欢我认为这就是问题,就像我说它看起来像位图没有得到正确的清理,而且它似乎发生在每一个视图上,但这个特别糟糕。
我也在每个视图上加载2个片段,一个是框架布局中的幻灯片菜单片段,另一个是一个导航栏片段,它只包含2个位图用于徽标和菜单句柄,所以那些也可能是罪魁祸首我想。这是导航栏片段:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
// return inflater.Inflate(Resource.Layout.YourFragment, container, false);
var view = inflater.Inflate(Resource.Layout.navbar, container, false);
var navLogo = view.FindViewById(Resource.Id.navbarLogo);
var menuHandle = view.FindViewById(Resource.Id.menuHandle);
var navSpacer = view.FindViewById(Resource.Id.navSpacer);
((ImageButton)(menuHandle)).SetMaxWidth(Shared.GenerateProportionalWidth(.25f, 50));
((ImageButton)(menuHandle)).SetMaxHeight(Shared.GenerateProportionalHeight(.25f, 50));
((ImageButton)(menuHandle)).Click += (object sender, EventArgs e) =>
{
var slideMenu = FragmentManager.FindFragmentById(Resource.Id.SlideMenuFragment);
if (slideMenu.IsHidden)
{
FragmentManager.BeginTransaction().Show(slideMenu).Commit();
}
else if (!slideMenu.IsHidden)
{
FragmentManager.BeginTransaction().Hide(slideMenu).Commit();
}
};
var navLogoParams = navLogo.LayoutParameters;
// Account for the padding offset of the handle to center logo truly in the center of the screen
navLogoParams.Width = global::Android.Content.Res.Resources.System.DisplayMetrics.WidthPixels - (((ImageButton)(menuHandle)).MaxWidth * 2);
navLogoParams.Height = (Shared.GenerateProportionalHeight(.25f, 30));
navLogo.LayoutParameters = navLogoParams;
// Spacer puts the logo in the middle of the screen, by making it's size the same as the handle on the opposite side to force-center the logo
((Button)(navSpacer)).SetMaxWidth(Shared.GenerateProportionalWidth(.25f, 50));
((Button)(navSpacer)).SetMaxHeight(Shared.GenerateProportionalHeight(.25f, 50));
return view;
}
有没有人看到我正在犯的任何明显或愚蠢的错误?我觉得只是纯粹的缺乏经验导致我错过了一些非常明显的东西,或者我做错了什么,无论如何。
编辑#1:
位图泄漏中的1个是导航片段中的菜单句柄按钮,因此将泄漏从300kb降低到200kb,但我仍然需要弄清楚如何正确清理它。
编辑#2:
这是我的代码,可以缩放位图
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight)
{
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.InJustDecodeBounds = true;
BitmapFactory.DecodeResource(res, resId, options);
// Calculate inSampleSize
options.InSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return BitmapFactory.DecodeResource(res, resId, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// Raw height and width of image
int height = options.OutHeight;
int width = options.OutWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth)
{
int halfHeight = height / 2;
int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth)
{
inSampleSize *= 2;
}
}
return inSampleSize;
}
答案 0 :(得分:1)
有时候Bitmaps不能正确收集垃圾,并且会产生异常存储异常。
我的建议是,如果你正在使用位图就是打电话
System.gc();
正确地从内存中回收位图
答案 1 :(得分:0)
对于任何想知道的人,我已经找到了问题所在。 Xamarin是原生java的ac#包装器,因此在运行时有本机Java运行时和单声道运行时,所以任何像你想要清理的位图这样的对象,你需要清理本机Java对象,但你也是需要清理本机对象的c#句柄,因为垃圾收集器会查看它是否应该清理资源,查看与资源关联的句柄,然后继续。我的解决方案是在清理本机Java对象之后调用c#dispose,然后同时调用c#和Java垃圾收集器,我不确定是否明确需要调用两个垃圾收集器,但我选择这样做无论如何。真的希望这可以帮助别人,我不羡慕那些不得不追捕这些问题的人。