无法定位 Android 应用程序内存泄漏的原因

特雷弗·哈特

我构建了整个应用程序认为垃圾收集器处理内存清理很好,这对我来说非常愚蠢和天真,但是嘿,这是我第一次使用 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 个屏幕,我这样做的原因是因为当屏幕尺寸较大时,它需要将所有这些显示为一个单一视图,因此在较大的屏幕上,它实际上会同时显示列表视图和网络视图,但仍会重新加载活动以加载新数据。

适配器代码可能给我带来了困难,但我不确定,我尝试了很多东西,但似乎没有任何帮助,这是适配器代码:

    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;
    }
}

所以我在这里使用 viewholder 模式,并在每个列表项生成时将点击事件分配给它们,并使用 nohistory 和 newtask 作为意图标志,以便页面正确刷新。为了清理位图,我一直在使用以下两种方法:

这会清除详细信息 webview 上的大图像:

        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();
    }

这会清除存储在每个列表视图项中的位图:

        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:

泄漏的位图之一是导航片段中的菜单句柄按钮,因此泄漏从 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;
    }
特雷弗·哈特

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

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

Android 应用程序和内存泄漏

来自分类Dev

VueJS - 应用程序无法定位路线

来自分类Dev

通过 Android Studio 的“无法定位 adb”错误

来自分类Dev

了解Android应用程序中的内存泄漏

来自分类Dev

Android我的应用程序内存泄漏吗?

来自分类Dev

了解Android应用程序中的内存泄漏

来自分类Dev

Android我的应用程序内存泄漏吗?

来自分类Dev

无法找出C应用程序中的内存泄漏

来自分类Dev

如何在仍然收到点击事件的同时使OS X应用程序无法定位?

来自分类Dev

更新为Android Marshmallow-现在,Android Studio无法定位设备

来自分类Dev

Android - 应用程序上下文可以解决内存泄漏吗?

来自分类Dev

在 Android 应用程序中使用 Facebook AccountKit 登录时出现内存泄漏

来自分类Dev

搜索框引导程序4无法定位在导航栏中的中间

来自分类Dev

无法隔离Scala批量数据加载应用程序中的JDBC内存泄漏

来自分类Dev

没有应用程序的高内存使用率。无法在Android Studio上使用

来自分类Dev

Android无法终止应用程序。应用程序重启

来自分类Dev

Android无法终止应用程序。应用程序重启

来自分类Dev

Android应用程序中的内存管理

来自分类Dev

Android应用程序内存异常?

来自分类Dev

Android应用程序中的内存管理

来自分类Dev

Android应用程序内存异常?

来自分类Dev

测试android应用程序时无法更改某些手机上的图标。可能是什么原因?

来自分类Dev

Android Studio - 无法调试应用程序

来自分类Dev

使浮动QDockWidget无法定位

来自分类Dev

IllegalArgumentException: 无法定位 adb

来自分类Dev

无法定位跨度元素

来自分类Dev

无法定位硒元素

来自分类Dev

Pydub 无法定位 ffprobe

来自分类Dev

无法确定应用程序崩溃的原因

Related 相关文章

热门标签

归档