2017-03-03 1 views
1

nicht zu finden Ich baute diese ganze App dachte, dass die Garbage Collector behandelt Speicherbereinigung ganz gut, die war unglaublich dumm und naiv von mir, aber hey, es war mein erstes Mal alle Ich benutze Xamarin, um eine App zu erstellen, und das erste Mal, dass ich eine App erstelle, also was soll ich machen? Jeder Bildschirm scheint Speicher zu verlieren, aber die Bildschirme, die die meisten sind Bildschirme auslaufen, die Bitmaps haben, um ein Speicherabbild zu erzeugen und es in MAT Analyse, fand ich folgendes:Ursache des Speicherverlusts in Android App

enter image description here

So gibt es vier potentielle Täter , 2 sind Bitmaps, 2 sind Byte-Arrays. Dies ist ein Heap-Dump für das Hauptmenü der App, wenn ich in meine List-View-Aktivität zum Auflisten von Elementen gehe, bekomme ich 5 potentielle Lecks aus Bitmaps. Hier ist der Code für die Aktivität:

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

Der wichtige Teil, um dies zu beachten ist die Art und Weise dieser Tätigkeit arbeitet, ist es einen Adapter lädt, und wenn es zeigt es eine Liste der Elemente, wenn ein Element geklickt wird , es lädt die gleiche Aktivität neu und berechnet die Bildschirmgröße, verkleinert die Liste, um nur die Seitenansicht zur Seite zu zeigen, und zeigt Details über das Element an, so dass 2 Bildschirme simuliert werden, der Grund dafür ist der Bildschirm Die Größe ist größer, es muss alles in einer einzigen Ansicht angezeigt werden. Auf größeren Bildschirmen werden also sowohl die Listenansicht als auch die Webansicht angezeigt, aber die Aktivität wird erneut geladen, um neue Daten zu laden.

Die Code-Adapter ist wahrscheinlich das, was mir eine harte Zeit zu geben, aber ich bin nicht sicher, ich habe ein paar Dinge ganz versucht, aber nichts scheint zu helfen, hier ist der Adapter Code:

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

Also verwende ich hier das Viewholder-Muster und weise jedem Listenelement Click-Events zu, wenn sie erzeugt werden, mit nohistory und newtask als Intentions-Flags, damit die Seiten korrekt aktualisiert werden. Um die Bitmaps aufzuräumen, ich habe diese beiden Methoden unter Verwendung von:

Diese reinigt das große Bild auf dem Details Webansicht:

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

Und das reinigt die gespeicherten Bitmaps in jedem Listenansicht Artikel:

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

Sie dann wie so in den Aktivitäten OnDestroy Methode aufgerufen:

 protected override void OnDestroy() 
    { 
     base.OnDestroy(); 
     CleanBitmap(); 
     CleanListViewBitmaps(); 
     Shared.appleSelected = false; 
    } 

Ich verwende auch eine gemeinsam genutzte Klasse mit statischen Variablen, um im Wesentlichen Ansichtszustände zu verfolgen, wie wenn etwas ausgewählt wurde oder nicht, aber es speichert nur Grundelemente, es speichert keine Ansichtsobjekte oder ähnliches, also denke ich nicht Das ist das Problem, wie ich sagte, es sieht so aus, als ob Bitmaps nicht richtig gereinigt werden, und es scheint bei jeder Ansicht zu passieren, aber diese ist besonders schlecht.

Ich auch auf jeder Ansicht laden 2 Fragmente, eine ist ein Dia-Menü Fragment in einem Rahmenlayout, und die andere ist ein Navbar-Fragment, das nur 2 Bitmaps für ein Logo und Menü enthält, so dass auch Täter ich sein könnte annehmen. Hier ist das Navbar-Fragment:

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

Seht jemand irgendeinen offensichtlichen oder dummen Fehler, den ich mache? Ich habe das Gefühl, dass es reine Unerfahrenheit sein muss, die mich dazu bringt, etwas wirklich Offensichtliches zu verpassen, oder ich mache etwas völlig Falsches, egal wie.

EDIT # 1:

1 der Bitmaps undichten das Menü-Handle-Button in dem Navigations Fragmente war, so dass das Leck nach unten von 300kb auf 200kb fällt, aber ich muß noch herausfinden, wie es zu reinigen richtig .

EDIT # 2:

Hier ist mein Code, die Bitmaps nach unten

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

Antwort

0

Für alle fragen, ich habe das Problem gelöst. Xamarin ist ein # Wrapper um natives Java, also gibt es zur Laufzeit die native Java Runtime und auch die mono Runtime, also jedes Objekt wie eine Bitmap, die Sie bereinigen wollen, müssen Sie das native Java-Objekt bereinigen, aber Sie auch müssen den C# -Speicher auf das native Objekt aufräumen, denn was passiert, ist der Garbage Collector, um zu sehen, ob er Ihre Ressource bereinigen soll, ein Handle sehen wird, das der Ressource zugeordnet ist, und weitergeht. Meine Lösung war, die C# dispose aufzurufen, nachdem ich das native Java-Objekt bereinigt und dann sowohl den C# - als auch den Java-Garbage Collector aufgerufen habe. Ich bin mir nicht sicher, ob der Aufruf beider Garbage Collectors explizit erforderlich ist, aber ich entschied mich trotzdem dafür. Ich hoffe ernsthaft, dass dies jemandem hilft, ich beneide niemanden, der diese Probleme aufdecken muss.

1

Manchmal Bitmaps ar nicht Müll richtig gesammelt Waagen und generete die OutOfMemory Ausnahme.

mein Vorschlag, wenn Sie mit Bitmaps arbeiten sind ist System.gc(); zu nennen Bitmaps aus richtig Speicher zu recyceln

+0

Das Problem ist, dass die Bildansichten Garbage Collector Roots sind, so dass sie nicht wirklich Müll gesammelt bekommen, weil etwas auf sie verweist, funktioniert meine Methode der Reinigung Bitmaps wie oben gezeigt, durch explizites Nullen und recycling sie, aber dann kann ich Verwende die Bitmaps nicht, weil sie recycelt wurden. –

+1

Die andere Option, um zu verhindern, dass das Leck erzeugt wird, ist das Laden des Bildes im Hintergrund, das heißt, Sie müssen eine asynchrone Aufgabe erstellen, die im Hintergrund den Image-Bitmap-Prozess bekommt und bei der Ausführung aktualisiert die Ansicht, die Sie hier rot machen können Sie können im Hintergrund tun, um es effizient zu machen https://developer.android.com/topic/performance/graphics/load-bitmap.html –

+0

Werft einen Blick, danke für den Tipp! –

Verwandte Themen