使用Xamarin進行開發(fā)的朋友鲜漩,不必說,肯定是看中了這項技術(shù)所具有的跨平臺特性缩幸,否則也不會跟我一樣壹置,棄官方正統(tǒng)不用,研究這種旁門左道表谊。而今天我準(zhǔn)備在這篇文章中介紹的是我在使用Xamarin.iOS開發(fā)時遇到的幾個大坑钞护,特號適合給從Android開發(fā)轉(zhuǎn)過的朋友看,因為坑最可怕之處在于爆办,在你掉進去之間你始終堅定地相信那里是平坦的难咕,那種像湯姆走進了杰米的陷坑,探腳試了半天才驚覺下面竟然是空距辆,然后大叫一聲余佃,轟然墜下。而對本來就是iOS開發(fā)者的朋友跨算,本文所述之坑你怕早已趟過了咙冗,不過溫故而知,偶爾重溫一下惡夢也是很美好的漂彤。
言歸正傳雾消,咱們進入第一個坑:循環(huán)引用問題灾搏。
所謂循環(huán)引用,最典型的就是兩個對象持有彼此的引用立润,從而可能導(dǎo)致內(nèi)存泄露狂窑,如下圖。
典型的循環(huán)引用寫法如下:
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而言卻已經(jīng)不是問題了桑腮,因為Java所使用的垃圾收集器有能力發(fā)現(xiàn)從根頂點不可達的對象泉哈,即使是出現(xiàn)循環(huán)引用,Java的GC也可以回收這一部分內(nèi)存破讨。而要命的事情正是由此而來——我把這種認(rèn)知帶到了Xamarin.iOS的開發(fā)中來丛晦。我本以為C#在垃圾收集器能力上不應(yīng)該遜色于Java才對,卻沒有想到垃圾這個東西怎么回收提陶,終究還是得聽平臺的烫沙,而iOS平臺所使用的自動引用計數(shù)(ARC)技術(shù)對循環(huán)引用是無能為力的∠栋剩看來進入一個新領(lǐng)域時锌蓄,切不可帶著那些先入為主的想法。
針對上述的代碼撑柔,Xamarin官方給出循環(huán)引用的可選解決方法有四個:
- 通過手動將container置為null來打破引用環(huán)瘸爽;
- 手動將被包含的對象從container中移除;
- 調(diào)用Dispose釋放對象铅忿;
- 對container使用弱引用來避免形成引用環(huán)剪决;
雖然以上四種方法對付這類循環(huán)引用是有效的,但事實上這種寫法本身就不合理檀训,應(yīng)該避免柑潦,請參考避免引用環(huán)的原則(Rules to avoid retain cycles)
此外,還有一些情況的循環(huán)引用發(fā)生得比較隱晦,比如閉包中的循環(huán)引用肢扯。
在沒有注意循環(huán)引用問題之前妒茬,我是這樣添加點擊事件的:
button.AddTarget((sender, e) => {
button.doSomething();
}, UIControlEvent.TouchUpInside);
但這種做法是錯誤的担锤,因為我在閉包中引用了button蔚晨,閉包會一直持有一個button的引用,從而形成了循環(huán)引用肛循。
正確的做法應(yīng)該是:
button.AddTarget((sender, e) => {
((UIButton)sender).doSomething();
}, UIControlEvent.TouchUpInside);
這里針對UIView的內(nèi)存回收問題铭腕,我寫了一個粗暴而有效的擴展方法:
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();//可使該對象的內(nèi)存被立即回收累舷;
}
}
}
在保證不形成循環(huán)引用的情況下,Dispose方法可以不調(diào)用夹孔,但調(diào)用該方法是有額外好處的被盈,首先可使內(nèi)存被立即釋放而不必等待GC的回收節(jié)點析孽,其次是即便形成了循環(huán)引用,Dispose方法可以確保內(nèi)存會被釋放掉只怎。這里交代一下我使用這個方法的場景袜瞬。我目前在開發(fā)iOS輸入法,輸入法界面上的控件切換非常頻繁身堡,本來是應(yīng)該將常用的控件緩存起來的邓尤,但iOS給Extension規(guī)定的內(nèi)存上限非常嚴(yán)苛,據(jù)說是40m贴谎,過線即死汞扎,所以我惟有在控件用完之后以迅速而可靠的方式把內(nèi)存釋放掉。
對于定制的UIView擅这,最好是重載它們的Dispose(bool)方法澈魄,做一些清理工作;
class MyView : UIView
{
/*官方給的示例中重載了Dispose(void)方法蕾哟,
*但事實上這個方法已經(jīng)不可重載一忱,能重載的只有Dispose(bool)方法,
*不知道這算不算坑呢谭确?
*/
protected override void Dispose(bool disposing)
{
//do something;
base.Dispose (disposing);
}
}
還有一個坑是:在Xamarin中帘营,某些類型的工程下有一部分標(biāo)準(zhǔn)的API是不可用的。這并不是你的工程配置有什么問題逐哈,而是在.Net Portable Subset中就沒有這種API芬迄,比如Console和Thread,如果你嘗試調(diào)用他們昂秃,你會看到下面這種提示禀梳。
解決方法是用其它API替代它們。
- Console.WriteLine(string)可以用Debug.WriteLine(string)替代肠骆;
- Thread可以用Task<T>類替代算途。
可能還有其它一些標(biāo)準(zhǔn)的API也不可用,但目前我只遇到這兩個蚀腿,如果再遇到我會在這里更新的嘴瓤,當(dāng)然新坑也是如此。
2017-01-21更新:
當(dāng)我使用iOS中的NSUserDefaults取值時莉钙,取出的nint不能隱式轉(zhuǎn)換成為C#的int類型廓脆。如果直接使用nint賦給int類型參數(shù)會導(dǎo)致程序崩潰。
void doSomeThing(int i){
}
var i = NSUserDefaults.StandardUserDefaults.IntForKey(keyStr);//會崩潰磁玉;
//正確寫法:
//int i = (int)NSUserDefaults.StandardUserDefaults.IntForKey(keyStr);
doSomeThing(i);