使用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官方给出循环引用的可选解决方法有四个:

  1. 通过手动将container置为null来打破引用环;
  2. 手动将被包含的对象从container中移除;
  3. 调用Dispose释放对象;
  4. 对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,如果你尝试调用他们,你会看到下面这种提示。

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


参考文章