无法在Android应用

时间:2017-03-03 20:26:14

标签: c# android memory xamarin memory-leaks

我构建了整个应用程序,认为垃圾收集器处理内存清理就好了,这对我来说非常愚蠢和天真,但是,嘿,这是我第一次使用Xamarin构建应用程序,而我的第一次是时候建立一个应用程序了,那么一个人要做什么?每个屏幕似乎泄漏内存,但泄漏最多的屏幕是具有位图的屏幕,生成内存转储并在MAT中进行分析,我发现以下内容:

enter image description here

因此有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;
    }

2 个答案:

答案 0 :(得分:1)

有时候Bitmaps不能正确收集垃圾,并且会产生异常存储异常。

我的建议是,如果你正在使用位图就是打电话 System.gc(); 正确地从内存中回收位图

答案 1 :(得分:0)

对于任何想知道的人,我已经找到了问题所在。 Xamarin是原生java的ac#包装器,因此在运行时有本机Java运行时和单声道运行时,所以任何像你想要清理的位图这样的对象,你需要清理本机Java对象,但你也是需要清理本机对象的c#句柄,因为垃圾收集器会查看它是否应该清理资源,查看与资源关联的句柄,然后继续。我的解决方案是在清理本机Java对象之后调用c#dispose,然后同时调用c#和Java垃圾收集器,我不确定是否明确需要调用两个垃圾收集器,但我选择这样做无论如何。真的希望这可以帮助别人,我不羡慕那些不得不追捕这些问题的人。