使用Xamarin进行开发的朋友,不必说,肯定是看中了这项技术所具有的跨平台特性,否则也不会跟我一样,弃官方正统不用,研究这种旁门左道。而今天我准备在这篇文章中介绍的是我在使用Xamarin.iOS开发时遇到的几个大坑,特号适合给从Android开发转过的朋友看,因为坑最可怕之处在于,在你掉进去之间你始终坚定地相信那里是平坦的,那种像汤姆走进了杰米的陷坑,探脚试了半天才惊觉下面竟然是空,然后大叫一声,轰然坠下。而对本来就是iOS开发者的朋友,本文所述之坑你怕早已趟过了,不过温故而知,偶尔重温一下恶梦也是很美好的。
言归正传,咱们进入第一个坑:循环引用问题。
所谓循环引用,最典型的就是两个对象持有彼此的引用,从而可能导致内存泄露,如下图。
典型的循环引用写法如下:
class Container : UIView
{
}
class MyView : UIView
{
object parent;
public MyView (object parent)
{
this.parent = parent;
}
}
var container = new Container ();
container.AddSubview (new MyView (container));
不过这个问题对于Java而言却已经不是问题了,因为Java所使用的垃圾收集器有能力发现从根顶点不可达的对象,即使是出现循环引用,Java的GC也可以回收这一部分内存。而要命的事情正是由此而来——我把这种认知带到了Xamarin.iOS的开发中来。我本以为C#在垃圾收集器能力上不应该逊色于Java才对,却没有想到垃圾这个东西怎么回收,终究还是得听平台的,而iOS平台所使用的自动引用计数(ARC)技术对循环引用是无能为力的。看来进入一个新领域时,切不可带着那些先入为主的想法。
针对上述的代码,Xamarin官方给出循环引用的可选解决方法有四个:
- 通过手动将container置为null来打破引用环;
- 手动将被包含的对象从container中移除;
- 调用Dispose释放对象;
- 对container使用弱引用来避免形成引用环;
虽然以上四种方法对付这类循环引用是有效的,但事实上这种写法本身就不合理,应该避免,请参考避免引用环的原则(Rules to avoid retain cycles)
此外,还有一些情况的循环引用发生得比较隐晦,比如闭包中的循环引用。
在没有注意循环引用问题之前,我是这样添加点击事件的:
button.AddTarget((sender, e) => {
button.doSomething();
}, UIControlEvent.TouchUpInside);
但这种做法是错误的,因为我在闭包中引用了button,闭包会一直持有一个button的引用,从而形成了循环引用。
正确的做法应该是:
button.AddTarget((sender, e) => {
((UIButton)sender).doSomething();
}, UIControlEvent.TouchUpInside);
这里针对UIView的内存回收问题,我写了一个粗暴而有效的扩展方法:
public static class SuiHanUIViewExtention {
public static void removeAllSubViews(this UIView view) {
var temp = view.Subviews;
if (temp == null) return;
foreach (var i in temp) {
if (i == null) continue;
i.RemoveFromSuperview();
i.removeAllSubViews();//递归,将子控件所包含的控件也一并移除;
i.Dispose();//可使该对象的内存被立即回收;
}
}
}
在保证不形成循环引用的情况下,Dispose方法可以不调用,但调用该方法是有额外好处的,首先可使内存被立即释放而不必等待GC的回收节点,其次是即便形成了循环引用,Dispose方法可以确保内存会被释放掉。这里交代一下我使用这个方法的场景。我目前在开发iOS输入法,输入法界面上的控件切换非常频繁,本来是应该将常用的控件缓存起来的,但iOS给Extension规定的内存上限非常严苛,据说是40m,过线即死,所以我惟有在控件用完之后以迅速而可靠的方式把内存释放掉。
对于定制的UIView,最好是重载它们的Dispose(bool)方法,做一些清理工作;
class MyView : UIView
{
/*官方给的示例中重载了Dispose(void)方法,
*但事实上这个方法已经不可重载,能重载的只有Dispose(bool)方法,
*不知道这算不算坑呢?
*/
protected override void Dispose(bool disposing)
{
//do something;
base.Dispose (disposing);
}
}
还有一个坑是:在Xamarin中,某些类型的工程下有一部分标准的API是不可用的。这并不是你的工程配置有什么问题,而是在.Net Portable Subset中就没有这种API,比如Console和Thread,如果你尝试调用他们,你会看到下面这种提示。
解决方法是用其它API替代它们。
- Console.WriteLine(string)可以用Debug.WriteLine(string)替代;
- Thread可以用Task
类替代。
可能还有其它一些标准的API也不可用,但目前我只遇到这两个,如果再遇到我会在这里更新的,当然新坑也是如此。
2017-01-21更新:
当我使用iOS中的NSUserDefaults取值时,取出的nint不能隐式转换成为C#的int类型。如果直接使用nint赋给int类型参数会导致程序崩溃。
void doSomeThing(int i){
}
var i = NSUserDefaults.StandardUserDefaults.IntForKey(keyStr);//会崩溃;
//正确写法:
//int i = (int)NSUserDefaults.StandardUserDefaults.IntForKey(keyStr);
doSomeThing(i);
参考文章
- Java的内存泄漏
- 理解 ARC 下的循环引用
- Xamarin官方对iOS开发的性能优化建议(Xamarin.iOS Performance)
- Xamarin官方对跨平台开发的性能优化建议(Cross-Platform Performance)
- 避免引用环的原则(Rules to avoid retain cycles)
- 扩展方法(C# 编程指南)
- Xamarin.iOS中更深层次的坑(Xamarin.iOS, the garbage collector and me)