In this Xamarin forum post, Xamarin admin Clancey says:
But as a general rule, anything that has that type of interaction say
a UIButton, you really need to keep a reference to it.
For example:
//This can will blow up
public override ViewDidLoad()
{
var myButton = new UIButton();
myButton.Tapped += delegate{
//do something
};
View.AddSubview(myButton);
}
This is because myButton goes out of scope and is GC'ed, but the underlying iOS button handle is still alive, and if ObjectiveC calls back to Mono with that button handle, the app crashes. So you need to make myButton a class level variable, so it stays alive.
My question is: what about other kinds of UIKit objects, like UIImage? Suppose I do this:
var image = new UIImage(sFilename);
ctlImageView = new UIImageView(image);
this.View.Add(ctlImageView);
ctlImageView is at class scope, but image isn't. Can this cause the same kind of memory leak, and does image also need to be at class scope? Or are UIView objects special in this regard?
You don't need to keep a reference to the UIImage because the UIImageView will hold one for you and clean it up when it is disposed.
Related
From my few years of experience programming in graphics, one thing that I have learned is that you should never pass in a reference to a graphics context to an object and operate on it for the duration of the program (JOGL explicitly states this). A context can be invalidated when something such as the graphics device (GPU) is reset, turned off, or some other weird thing happens.
I have recently delved back into programming in XNA 4.0, and one of my projects involves objects needing to know about the size of the window/viewport, when the window is resized, and when dynamic buffers have lost their content (requiring the buffers to be rebuilt on a possibly invalidated GraphicsDevice). Instead of passing in the GraphicsDevice and GameWindow to numerous methods in the update phase or for Disposal, I have opted to pass them into constructors. For example:
public Camera(GameWindow w, GraphicsDeviceManager m) {
// ... Yada-yada matrices
gdm = m;
window = w;
window.ClientSizeChanged += OnWindowResize;
}
public void Dispose() {
window.ClientSizeChanged -= OnWindowResize;
window = null;
gdm = null;
}
// Control Logic ...
public void OnWindowResize(object Sender, EventArgs args) {
Vector2 s = new Vector2(gdm.GraphicsDevice.Viewport.TitleSafeArea.Width, gdm.GraphicsDevice.Viewport.TitleSafeArea.Height);
// Recalculate Projection ...
}
Is it safe to do something like this, or is something happening in the background that I need to be aware of?
I solved this problem in my current game project by running the game as a singleton, which makes it available in a static context within the namespace. Game.Instance.graphicsDevice will always point to the current graphics device object, even if the context has changed. XNA raises various events when the context is invalidated/changed/reset/etc., and you can reload/re-render things and resize buffers as needed by hooking in to these events.
Alternatively, you could pass GraphicsDevice with the ref keyword, which might be a quick, drop-in fix by simply being the same reference as the original caller, assuming that caller that instantiated your objects either has the original reference object or had the GraphicsDevice passed to it with ref as well.
Is there a way to manipulate the size of a non-displayed UIView from outside the UI thread prior to adding it to a displayed view?
While working through some asynchronous iOS code, I thought I would try to have an async method build up a UIView that would be displayed later [on the UI thread]. In this case, and this appears to be the "gotcha", it was a UILabel where I want to give it a predetermined frame size derived from a StringSize call. Unfortunately, the UIView constructor that takes a RectangleF frame calls UIApplication.EnsureOnUIThread first.
// Throws UIKitThreadAccessException on Frame-setting UILabel constructor.
Task<UILabel> getView = Task.Factory.StartNew(() => {
//... Do some async fun (e.g., call web service for some data for someNSString)
SizeF requiredStringSize = someNSString.StringSize(someFont, new SizeF(maxWidth, float.MaxValue), UILineBreakMode.WordWrap);
RectangleF someViewFrame = new RectangleF(PointF.Empty, requiredStringSize)
return new UILabel(someViewFrame);
});
Since I don't really need to set a valid location at the point of this task execution, I figured I could avoid setting the frame in the constructor and set the size afterwards. Unfortunately, you only seem to be able to set size by modifying UIView.Frame as a whole. While the parameter-less constructor does not make this UI thread call, as soon as I try to set the Frame to the size needed, the UIView.Frame accessor does and it blows up.
// Also throws UIKitThreadAccessException, this time when setting the Frame directly.
Task<UILabel> getView = Task.Factory.StartNew(() => {
//...do all the above stuff...
UIView someView = new UILabel();
someView.Frame = new RectangleF(someView.Frame.Location, requiredStringSize);
});
I've already decided to make my code more specific to the case at hand and use a Task<string> instead, letting the displaying code (run on the UI thread) handle the view creation, but it would be nice to know if this is possible since it would make the code I am writing more extensible.
UIKit is not designed to be used outside of the main thread.
I've seen some bizarre behaviour creep in when this rule is ignored, so I strongly advise against this.
Note: I've created a simple project—you can see how switching types between UIButton and CustomButton in storyboard changes GC behavior.
I'm trying to get my head wrapped around MonoTouch garbage collector.
The issue is similar to the one fixed in MT 4.0, however with inherited types.
To illustrate it, consider two view controllers, parent and child.
Child's view contains a single UIButton that writes to console on tap.
Controller's Dispose method throws an exception so it's hard to miss.
Here goes child view controller:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
sayHiButton.TouchUpInside += (sender, e) =>
SayHi();
}
}
void SayHi()
{
Console.WriteLine("Hi");
}
protected override void Dispose (bool disposing)
{
throw new Exception("Hey! I've just been collected.");
base.Dispose (disposing);
}
Parent view controller just presents child controller and sets a timer to dismiss it and run GC:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
var child = (ChildViewController)Storyboard.InstantiateViewController("ChildViewController");
NSTimer.CreateScheduledTimer(2, () => {
DismissViewController(false, null);
GC.Collect();
});
PresentViewController(child, false, null);
}
If you run this code, it predictably crashes inside ChildViewController.Dispose() called from its finalizer because child controller has been garbage collected. Cool.
Now open the storyboard and change button type to CustomButton. MonoDevelop will generate a simple UIButton subclass:
[Register ("CustomButton")]
public partial class CustomButton : UIButton
{
public CoolButton (IntPtr handle) : base (handle)
{
}
void ReleaseDesignerOutlets()
{
}
}
Somehow changing the button type to CustomButton is enough to trick garbage collector into thinking child controller is not yet eligible for collection.
How is that so?
This is an unfortunate side-effect of MonoTouch (who is garbage collected) having to live in a reference counted world (ObjectiveC).
There are a few pieces of information required to be able to understand what's going on:
For every managed object (derived from NSObject), there is a corresponding native object.
For custom managed classes (derived from framework classes such as UIButton or UIView), the managed object must stay alive until the native object is freed [1]. The way it works is that when a native object has a reference count of 1, we do not prevent the managed instance from getting garbage collected. As soon as the reference count increases above 1, we prevent the managed instance from getting garbage collected.
What happens in your case is a cycle, which crosses the MonoTouch/ObjectiveC bridge and due to the above rules, the GC can't determine that the cycle can be collected.
This is what happens:
Your ChildViewController has a sayHiButton. The native ChildViewController will retain this button, so its reference count will be 2 (one reference held by the managed CustomButton instance + one reference held by the native ChildViewController).
The TouchUpInside event handler has a reference to the ChildViewController instance.
Now you see that the CustomButton instance will not be freed, because its reference count is 2. And the ChildViewController instance will not be freed because the CustomButton's event handler has a reference to it.
There are a couple of ways to break the cycle to fix this:
Detach the event handler when you no longer need it.
Dispose the ChildViewController when you no longer need it.
[1] This is because a managed object may contain user state. For managed objects which are mirroring a corresponding native object (such as the managed UIView instance) MonoTouch knows that the instance can not contain any state, so as soon as no managed code has a reference to the managed instance, the GC can collect it. If a managed instance is required at a later stage, we just create a new one.
I think I'm getting close to understanding how Mono GC and ObjC ref counting live together.
The way it works is that when a native object has a reference count of 1, we do not prevent the managed instance from getting garbage collected. As soon as the reference count increases above 1, we prevent the managed instance from getting garbage collected.
This is because a managed object may contain user state. For managed objects which are mirroring a corresponding native object (such as the managed UIView instance) MonoTouch knows that the instance can not contain any state, so as soon as no managed code has a reference to the managed instance, the GC can collect it. If a managed instance is required at a later stage, we just create a new one.
So if I create a CustomButton that inherits UIButton, add it as subview to my View, let the managed reference slip out of scope and then run GC, this managed CustomButton still won't be eligible for collection.
Why can't it be collected? Of course it may have managed state like properties, but if there is no link to it from managed objects, who cares about this state? It may as well just disappear, why can't it?
I'm thinking of one possible reason: subscribing to CustomButton events won't keep it alive for the GC so when the object gets collected, events stop firing. This would perhaps result in unexpected behavior.
Is this correct? Are there other reasons for keeping the managed object alive even if no one links it?
Why can't it be collected? Of course it may have managed state like properties, but if there is no link to it from managed objects, who cares about this state? It may as well just disappear, why can't it?
Native code might have references to the object, which may cause the object to resurface to managed code again later.
I believe a code sample would illustrate what would happen:
class MyView : UIView {
public string ImportantSecret;
}
class AppDelegate : UIApplicationDelegate {
UIViewController vc;
public override bool FinishedLaunching (UIApplication app,
NSDictionary options)
{
var myView = new MyView ();
myView.ImportantSecret = "MonoTouchRocks";
vc = new UIViewController ();
vc.View = new UIView ();
vc.View.AddSubView (myView);
// When this method returns the only place where myView is referenced
// is from inside the *native* Subviews collection.
BeginInvokeOnMainThread (() =>
{
Console.WriteLine (((MyView) vc.Subviews [0]).ImportantSecret);
// If the MyView instance was garbage collected and recreated
// automatically at this point, ImportantSecret would be null.
});
}
}
Important: this code is just to illustrate the reason why the GC can't collect managed objects which may have state. This particular sample would actually not forget the important secret, since the Subviews array is automatically cached in managed code - but this is not generally true.
I use the scrollview to display multipage view, which have more than 10 pages. (scrollview.PageEnabled=true)
And every single page in scrollview has about 6 sub-view(named:ABCUI) which every one is loaded from nib :
this.scrollview.DecelerationEnded+= scrollView_DecelerationEnded(...);
public void LoadSubView(int nPageNo)
{
if (this.PageLoaded(nPageNo))
return;
for (int i=0;i<6;i++)
{
ABCViewController abcUI=MonoTouch.Foundation.NSBundle.MainBundle.LoadNib ("ABCUI", this, null); //XIB file size: 20K
abcui.SetTitle(...);
abcui.SetXXXX(...);
abcui.frame = .. pageFrame.X += this.ScrollView.Frame.Width+nPage*...;
this.scrollview.addsubview(abcUI.view,...);
}
}
public void scrollView_DecelerationEnded (object sender, EventArgs e)
{
int nPageNo=(int)Math.Ceiling((this.ScrollView.ContentOffset.X+1)/this.ScrollView.Frame.Width);
this.LoadSubView(nPageNo +1);
this.LoadSubView(nPageNo - 1);
}
public void Button1Clicked(object sender, EventArgs e)
{
this.ClearViewsInScrollView();
this.LoadSubView(1);
}
When the user trigger the button1 click, it will load the first page into scrollview(only 1 page oncetime, but 1 page has 6 sub-view), and when user scroll the scrollview , it will load the next page.
But it will take a long time when load the first page or switch page in scrollview , so the user must waiting:
ipad1: about 1000ms
iPad2: about 600ms
in simulator: 100ms;
how to optimize the performance(reduce to less 300ms/ipad1)?
Very good question and excellent timing, since I have been working on something like this the past few days.
Now, I am not sure if this solution will get you < 300ms loading, however in theory it is faster.
(You'll see both "XIB" and "NIB" terms. I am referring to the same thing. After all, a NIB is a "compiled" XIB.)
The key to the whole thing is to prevent loading of each XIB file multiple times. There is no reason for it, since what you (we) basically need from it, are instances from the objects in the XIBs, and not the XIBs themselves occupying memory.
Fortunately, the iOS SDK provides the UINib class which can do what we want. With this class, we can create multiple instances of the contents of a XIB, without having to load the XIB itself each time, just once in the "beginning".
Here's how to do it:
First, create a UINib object for each XIB file you want.
// Loads a NIB file without instantiating its contents.
// Simplified here, but to have access to a NIB for the whole lifecycle of the app,
// create a singleton somewhere.
static UINib abcuiNib = UINib.FromName("ABCUI", NSBundle.MainBundle);
Second, after you have loaded the NIB into memory, you can now get the objects from it.
abcuiNib.InstantiateWithOwneroptions(this, new NSDictionary());
Note the "this" parameter. Since it is a view controller you want to load, the above line should be somewhere early in the object's life cycle, eg in the constructor:
public partial class ABCViewController : UIViewController
{
public ABCViewController()
{
// The first parameter is needed to set an owner (File's Owner) for the objects
// that will be instantiated.
// The second parameter is for various options. It does not accept null in MonoTouch,
// but you can just pass an empty NSDictionary.
// If you have all your outlets correctly set up, the following line is all
// that is needed.
abcuiNib.InstantiateWithOwneroptions(this, new NSDictionary());
}
// We don't need the following, as it will load the XIB every time
//public ABCViewController() : base("ABCUI", null) {}
// view controller implementation
}
Mind you, I have not tested the above, as so far I have tested it with various single objects in XIBs. If (and when) I use it with UIViewControllers in XIBs, this is the direction I will move in. I'll also prepare an article with more in-depth findings and info in due time.
Also note that the same "rules" apply, eg. you will still have to release all outlets in the ViewDidUnload override.
If after this, you do not find any improvement in performance, I think you will need to redesign your XIBs. Apple suggests it is better to have multiple XIBs with few objects in each one, instead of few XIBs packed with objects.
Useful reading: Apple docs on NIB management