I built out this whole app thinking that the garbage collector handled memory clean-up just fine, which was incredibly stupid and naive of me, but hey, it was my first time every using Xamarin to build an app, and my first time ever building an app, so what's a guy to do? Every screen seems to leak memory, but the screens that leak the most are screens that have bitmaps, generating a memory dump and analyzing it in MAT, I found the following:
So there are 4 potential culprits, 2 are bitmaps, 2 are byte arrays. This is a heap dump for the main menu of the app, if I go into my list view activity for listing out elements, I get 5 potential leaks from bitmaps. Here is the code for the activity:
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();
}
The important part to note about this is the way this activity works is it loads an adapter, and when it displays it shows a list of items, when an item is clicked, it reloads this same activity, and it computes the screen size, shrinks down the list to show only the webview off to the side, and displays details about the item, thus simulating 2 screens, the reason I did this is because when the screen size is larger, it needs to show all of this as one single view, so on larger screens it will actually show both the listview and the webview, but still reload the activity to load new data.
The adapter code is probably what is giving me a hard time, but I'm not sure, I've tried quite a few things, but nothing seems to help, here's the 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;
}
}
So I'm using the viewholder pattern here, and assigning click events to each list item as they get generated, with nohistory and newtask as the intent flags so that the pages refreshes properly. To clean up the bitmaps, I have been using these two methods:
This cleans the large image on the details 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();
}
And this cleans the bitmaps stored in each listview item:
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();
}
They then get called in the activities ondestroy method like so:
protected override void OnDestroy()
{
base.OnDestroy();
CleanBitmap();
CleanListViewBitmaps();
Shared.appleSelected = false;
}
I'm also using a shared class with static variables to essentially track view states like if something was selected or no, but it only stores primitives, it doesn't store any view objects or anything like that, so I don't think that is the problem like I said it looks like bitmaps aren't getting cleaned correctly, and it seems to happen on every view, but this one in particular is bad.
I also on each view load 2 fragments, one is a slide menu fragment in a frame layout, and the other is a navbar fragment that just holds 2 bitmaps for a logo and menu handle, so those could be culprits too I suppose. Here's the 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;
}
Does anyone see any obvious or stupid mistake that I'm making? I feel like it has to just be sheer inexperience that's causing me to miss something really obvious, or I'm doing something completely wrong, either way.
EDIT #1:
1 of the bitmaps leaking was the menu handle button in the navigation fragment, so that drops the leak down from 300kb to 200kb, but I still need to figure out how to clean it properly.
EDIT #2:
Here is my code that scales bitmaps down
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;
}
For anyone wondering, I've figured out the problem. Xamarin is a c# wrapper around native java, so at runtime there is the native Java runtime, and the mono runtime as well, so any object like a bitmap that you want to cleanup, you need to cleanup the native Java object, but you also need to clean up the c# handle to the native object, because what happens is the garbage collector goes to see if it should clean your resource, sees a handle associated with the resource, and moves on. My solution was to call the c# dispose after I cleaned up the native Java object, and then call both the c# and Java garbage collector, I'm not sure if calling both garbage collectors is explicitly needed, but I chose to do it anyway. Seriously hope this helps someone out, I do not envy people who have to hunt down these problems.
Sometimes Bitmaps ar not garbage collected correctly, and generete the outofmemory exception.
my suggestion if you're working with bitmaps is to call
System.gc();
to recycle bitmaps from memory correctly
I have a icon which has a few different sizes (16px, 32px, 64px). I am calling ToBitmap() on it, but it is always returning the 32px image. How do I retrieve the 64px one?
Does this help?
Icon sizedIcon = new Icon(Resources.ResourceIcon, new Size(64,64));
For anyone else stumbling upon the same problem, I've found a nice little solution.
Image img = new Icon(Properties.Resources.myIcon, width, height).ToBitmap()
This is a fairly painful limitation in the ResourceManager class. Its GetObject() method doesn't provide a way to pass extra arguments that would allow selecting the returned icon by size. A workaround is to add the icon to the project instead. Use Project + Add Existing Item, select your .ico file. Select the added icon and change the Build Action property to "Embedded Resource".
You can now retrieve the desired icon with code like this:
public static Icon GetIconFromEmbeddedResource(string name, Size size) {
var asm = System.Reflection.Assembly.GetExecutingAssembly();
var rnames = asm.GetManifestResourceNames();
var tofind = "." + name + ".ICO";
foreach (string rname in rnames) {
if (rname.EndsWith(tofind, StringComparison.CurrentCultureIgnoreCase)) {
using (var stream = asm.GetManifestResourceStream(rname)) {
return new Icon(stream, size);
}
}
}
throw new ArgumentException("Icon not found");
}
Sample usage:
var icon1 = GetIconFromEmbeddedResource("ARW04LT", new Size(16, 16));
var icon2 = GetIconFromEmbeddedResource("ARW04LT", new Size(32, 32));
Beware one possible failure mode: this code assumes that the icon was added to the same assembly that contains the method.
The following sets the icon size for all the buttons in the toolbar.
It relies on the resource name being stored in the button tag.
public void SetButtons(object toolstrip, int IconWidth, int IconHeight)
{
var ts = (ToolStrip) toolstrip;
var size = new System.Drawing.Size();
size.Height = IconSize;
size.Width = IconSize;
foreach (ToolStripButton tsBtn in ts.Items)
{
tsBtn.ImageScaling = ToolStripItemImageScaling.None;
var resourcename = (String) tsBtn.Tag;
if (resourcename != null)
{
var myIcon = (Icon) Properties.Resources.ResourceManager.GetObject(resourcename);
var newIcon = new Icon(myIcon, IconWidth, IconHeight);
tsBtn.Image = newIcon.ToBitmap();
}
}
}
The size is determined when you first create the Icon instance, so you need to specify which size you want to use when you create it, using one of the Icon constructors that take a Size parameter.
internal static class IconHelper {
public static Icon GetSize(this Icon icon, int width, int height) {
return icon.GetSize(new Size(width, height));
}
public static Icon GetSize(this Icon icon, Size size) {
using(var mem = new MemoryStream()) {
icon.Save(mem);
mem.Position = 0;
return new Icon(mem, size);
}
}
}
There is no built-in method in the .Net framework that does this.
Instead, you can use this library.
Im adding multiple MapIcons to my UWP app MapControl. I want to change the MapIcon size based map zoom level.
This works almost ok, but when I update marker sizes (call UpdatMarkerSizes()) not all MapIcons get new image.
I have around 200 MapIcons in my map, which I try to update. Is this some performance issue or how I should try to update all MapIcons?
Here is how I initially add MapIcons:
public void AddMapIcons(IReadOnlyCollection<IItem> items)
{
var icon = (CurrentMarkerIconSize == MarkerIconSize.Small) ? _markerSmall : _markerNormal;
foreach (var item in items)
{
var stopIcon = new MapIcon
{
Location = new Geopoint(item.Location),
NormalizedAnchorPoint = new Point(0.5, 0.5),
ZIndex = 5,
CollisionBehaviorDesired = MapElementCollisionBehavior.RemainVisible,
Image = icon
};
MapControl.MapElements.Add(stopIcon);
}
}
And here is my MapIcon Image update functionality, this is called when map zoomlevel is changed.
private void UpdatMarkerSizes(MarkerIconSize newSize)
{
var newImage = (newSize == StopIconSize.Small) ? _markerSmall : _markerNormal;
foreach (var element in MapControl.MapElements)
{
(element as MapIcon).Image = newImage;
}
}
UPDATE 1:
I just noticed that if I also update MapIcon NormalizedAnchorPoint, ZIndex and CollisionBehaviorDesired (set same value again), this issue is not so bad. Previously when I updated my markers (UpdatMarkerSizes()) there was always couple of markers which did not get the new image. Now after this update, I get rarely one or two markers which did not get new image.
Updated code:
private void UpdatMarkerSizes(MarkerIconSize newSize)
{
var newImage = (newSize == StopIconSize.Small) ? _markerSmall : _markerNormal;
foreach (var element in MapControl.MapElements)
{
var mapIconElement = element as MapIcon;
if(mapIconElement != null)
{
mapIconElement.Image = newImage;
mapIconElement.NormalizedAnchorPoint = new Point(0.5, 0.5);
mapIconElement.ZIndex = 5;
mapIconElement.CollisionBehaviorDesired = MapElementCollisionBehavior.RemainVisible;
}
}
}
UPDATE 2:
I think that my solution in update 1 is good enough for now. I have posted new question which is related to MapIcons and their image updating too: Dynamically update collection of MapIcons, update process gets out of sync?
i am working in wpf richtextbox.use the below function use for page up.
TextRange range10 = new TextRange(txtAppendValue.CaretPosition.GetLineStartPosition(1), txtptrCaret);
FrameworkContentElement fce1 = (range10.End.Parent as FrameworkContentElement);
if (fce1 != null)
{
fce1.BringIntoView();
range10 = null;
fce1 = null;
}
its working.After i clear the richtextbox and load some other content,at that time the content shown where the pageup happened in last time?how to clear when i reload the document.
Updated:
public void HideTopBorder()
{
TextPointer currentline = txtAppendValue.CaretPosition.GetLineStartPosition(1);
System.Windows.Rect rc = currentline.GetCharacterRect(LogicalDirection.Forward);
System.Windows.Point upperLeftCorner = rc.Location;
HitTestResult result = VisualTreeHelper.HitTest(txtAppendValue, upperLeftCorner);
if (result == null)
{
Thickness margin = txtAppendValue.Margin;
margin.Top = -20;
txtAppendValue.Margin = margin;
currentline = null;
}
else
{
currentline = null;
}
}
After the result null only pageup happened.at that time i clear the document and load an another document.But now show the richtextbox where the already panned point.not top of the document.
Regards
Arjun
Before you load a new document save the value of rtb.VerticalOffset. Then load the new document and reassign the stored value to rtb.VerticalOffset.
I have a windows store app that currently allows my users to pin and unpin tiles to the start menu. I am able to set a background and logo and text but the problem is I want the tile to instead of a static logo with text I want it to be sort of a live tile.
So it would go from one side with just an image and then flip and the other side would be my app information. I know you are able to do this for regular live tiles and using xml, but I am using c# and would like for this to work in my secondary tile.
Any help on how to go about that would be great.
Here is some code I used to create the secondary tile:
private async void PinButton_OnClick (object sender, RoutedEventArgs e)
{
var item = createdItem.SelectedItem;
if (item != null)
{
var logo = new Uri(item.image);
if (item != null)
{
var smallLogo = new Uri(item.image);
var wideLogo = new Uri(item.image);
}
string tileActivationArguments = logoSecondaryTileId + "WasPinnedAt=" +
DateTime.Now.ToLocalTime();
logoSecondaryTileId = item.ID+ counter.ToString();
counter++;
//Create the tile
SecondaryTile tile = new SecondaryTile(logoSecondaryTileId, item.Code, item.FirstName + " " + item.LastName,
tileActivationArguments, TileOptions.ShowNameOnLogo | TileOptions.ShowNameOnWideLogo, logo);
if (item.SelectedItem is Details)
{
tile.ForegroundText = ForegroundText.Light;
tile.BackgroundColor = Colors.Black;
bool isPinned =
await
tile.RequestCreateForSelectionAsync(GetElementRect((FrameworkElement)sender),
Placement.Default);
if (isPinned)
{
Messages.InvokeToast(Toast.Created);
UnpinButton.Visibility = Visibility.Visible;
pinButton.Visibility = Visibility.Collapsed;
}
else
{
Messages.InvokeToast(Toast.Error);
}
if (SecondaryTile.Exists(logoSecondaryTileId))
{
var dialog = new MessageDialog("Already exists!")
{
Title = "Unable to Pin Tile!"
};
dialog.Commands.Add(new UICommand("Okay", new UICommandInvokedHandler(CommandHandler)));
dialog.CancelCommandIndex = 1;
await dialog.ShowAsync();
}
}
}
}
You just need to define the tile using a template that has a front and back. See here for how to define secondary tiles with templates: Is it possible to use tile templates with secondary tiles in a Windows Store App?. See here for the list of possible templates and which have backs: http://msdn.microsoft.com/en-us/library/windows/apps/hh761491.aspx