關於如何寫UI及屏幕適配的一些技巧


因為公司開啟了一個新的iOS項目, 所以近期比較忙, 沒有更新博客,今天打算總結一下關於UI布局及屏幕適配的一些實戰技巧,尤其使用純代碼,會對提升效率及代碼易於維護等方面有明顯幫助,這里提到的沒有使用任何Xib, 如果不是在外包公司,也推薦大家多使用甚至完全使用純代碼布局UI,優缺點下面會說明,本文布局使用masonry。貨不太干,只是工作中的一點點小技巧與基礎知識,大家可以來分享更多的技巧。

下篇地址:http://www.jianshu.com/p/0c1d76e7ea1a

提綱:
1. 關於xib/storyboard 與 純代碼的對比 2. 一條規范(又提了一點關於命名的) 3. UI工廠類 與 代碼塊 4. 懶加載, View使用strong還是weak 5. 復雜界面要會分區,要會障眼法 6. masonry均布View,及其布局時約束依賴關系 7. 關於屏幕適配的一點技巧
1. 關於xib/storyboard 與 純代碼的對比

a. xib快,純代碼慢,但是在純代碼熟練的情況下, 並不會慢很多
b. xib不易於修改,怎么修改,就是今天讓一個View上的元素這么排布,明天就要換種排布方式,后天又要加些東西。。。
c. xib不靈活,什么叫靈活,一個View上有10個元素, 其中5個都是不一定出現的,並且它們不是集中的布局在哪個位置, 亂七八糟的,多一個少一個布局還都有點影響,這就知道靈活的重要性了
d. xib不利於屏幕適配,怎么適配,5s上一個View距左10像素,產品說6p上就要距左20才協調,xib上拖線布局怎么搞,約束拖出來改變它嗎,復雜View有10個需要這樣處理的地方呢。。。
e. xib寫多了純代碼手生,當然純代碼寫多了,拖線也有點不熟練,這個不作為純代碼更好的原因。。。
f. 但是!面試的時候如果你說我xib用的多,純代碼布局有點不熟練, 不好意思, 拜拜(本人經歷過),你說我都是純代碼布局的, xib會,不熟,大多公司聽到以前都是純代碼,那沒問題,因為他們公司也不用xib。。。

2. 一條規范(又提了一點關於命名的)

“ .h 和 .m 的類擴展里面不要隨便加東西,尤其 .h 里的東西一定要是必須放在這里,放在別處不行,實在有不太重要還必須放在這的,打好注釋 ”

這個東西無數次在項目中見到過隨意在這加東西的做法, 剛寫完當時還好, 一個月以后再看, 瞬間懵逼。。。
這是什么鬼。。。
當時為啥要寫這個量。。。
這怎么還有個沒用過的量。。。
這個東西要不要傳, 為啥A類用的時候穿了, B類就不傳了。。。

再提一下另外一個規范, “名字不要隨便起,弄個坑爹名字,自己隔天都不知道啥意思,是跟公司結了仇了還是怕泄露天機”


不算特別不規范的命名

為什么說這些命名不是特別不規范, 因為這里面雖然有些vBack啦, lbl啦, tbl啦, 不是那么容易理解, 但是好歹是lbl,都用lbl了,不過為啥非得把Label放前面,官方命名的時候比如btn.titleLabel,Label也是放在后面的啊, 也沒縮寫成什么lbl,我們就簡簡單單的叫nickNameLabel不行嗎。。。

說到命名就再多說一點, 如果一個復雜View內部布局的時候需要分割成幾部分,在能想出名字的情況下最好不要按位置命名,比如topView,midView之類,明天產品說把位置調一下,最下面的部分比較重要提到最上面,這怎么辦, 盡量想一想這部分大概負責什么,要表達個什么意思

3. UI工廠類 與 代碼塊

UI工廠類: 其實代碼很簡單,就是把對Label, Button等控件的屬性賦值封裝一下, 做到一行代碼就能創建一個VIew, 如下圖, 雖然這一句代碼有點長, 但是習慣之后寫個View是真心快


UI工廠類.h

UI工廠類.m

代碼塊: 代碼塊就是下圖的東西, 應該沒人不知道,不會添加隔壁百度


代碼塊

這個東西不光是UI布局用, 很多位置都比較方便, 我常用的有這樣幾個


懶加載

masonry定義過的一部分

masonry填空模式

尤其是純代碼masonry布局, 這樣的代碼塊會讓你布局的速度直逼甚至超越拖線, 只需要打出make就會出現已經定義好的各種約束, 比如要布局高度, 打出makeh, 回車, 就直接進入填空模式,tab切換填空即可

4. 懶加載, View使用strong還是weak

為什么要用懶加載, 有一種說法是用到的時候在創建,節省內存開銷,這種說法固然沒問題,但是對於大部分UI來說,基本遲早都會被創建。
所以,主要優點不在這里,本着<自己的事情自己做, 盡量不要影響他人>的做人原則,代碼也該這樣寫,你既然是個View,那你就把自己解決好再來見我, 我要用你的時候只需要self.testView就可以了,下面兩張圖對比一下就會看到區別了,第二張為剛到公司時上一任的大作,這里選取了一種比較看的清的貼出來


弱引用懶加載 masonry布局

上圖注意:masonry的block沒有進行copy,即當前對象沒有引用這個block,是局部的引用,不會形成循環引用的,可以不用weakSelf


混亂的還好的示例

對比一下即可看出來, 由於上圖使用中的控件均使用懶加載, 所以布局方法里連addSubView都不用寫了, 只需逐條布局即可, 下圖中創建控件, 屬性賦值, 添加到父視圖的代碼都混在一起, 並且還沒有使用masonry, 用上之后只會更亂。。。

View使用strong還是weak: 關於這個問題,其實還是有很多可以說一下的地方
懶加載寫法:

@interface ViewController () @property (nonatomic, weak) UIView *weakView; @property (nonatomic, strong) UIView *strongView; @end @implementation ViewController /** UI控件使用弱引用創建方法 1. UIView *weakView = [[UIView alloc] init]; 這句必須聲明一個局部變量, 不能用_weakView, 因為用 _weakView = [[UIView alloc] init], 等號右側創建了一個View之后,給了一個弱引用持有,相當於沒有持有,直接就釋放掉了 而 UIView *weakView = [[UIView alloc] init],等號左側的weakView默認是一個強引用,會暫時持有保住它,但是生命周期就在這個懶加載的大括號內,所有會有其他代碼配合, 使這個View存活下來, 不被釋放 2. _weakView = weakView; 這句代碼為屬性賦值, 以后在其他位置不管通過self.weakView還是_weakView才能找到這個View, 基本作用可以說等同於強引用的 _strongView = [[UIView alloc] init]; 3. [self.view addSubview:weakView], 第一條注釋中說了,UIView *weakView 的生命周期就是在這個{}內,那么如何保證出了括號依舊存在,就是要給這個View加到一個不會被釋放的View(不一定強引用弱引用)上,即self.view, 這樣就不會被釋放掉了 */ - (UIView *)weakView { if (!_weakView) { UIView *weakView = [[UIView alloc] init]; _weakView = weakView; weakView.backgroundColor = [UIColor redColor]; [self.view addSubview:weakView]; } return _weakView; } - (UIView *)strongView { if (!_strongView) { _strongView = [[UIView alloc] init]; _strongView.backgroundColor = [UIColor greenColor]; } return _strongView; }

布局時區別:

- (void)configView { WeakSelf(ws); //弱引用由於懶加載直接加到父視圖上,所以點語法完了直接調用masonry布局方法即可 [self.weakView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(ws.view); make.top.equalTo(ws.view).offset(100); make.size.mas_equalTo(CGSizeMake(100, 100)); }]; [self.view addSubview:self.strongView]; [self.strongView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(ws.view); make.bottom.equalTo(ws.view).offset(-100); make.size.mas_equalTo(CGSizeMake(100, 100)); }]; }

移除之后的區別(重點,涉及到理解強弱指針):執行上述代碼后,屏幕上出現一上一下兩個View, 點擊空白區域, 移除掉兩個View, 1秒后看一下,1秒后看是因為, 出了這個方法, 也就是運行到結束的括號之后, 才會把View移除掉, 注意看坐下控制台兩個View,weakView為nil, 即已被釋放, strongView還是存在, 因為即使從父視圖上移除, self本身對其還有一個強引用, 不會釋放掉,那么如果想把這個View釋放掉需要怎么辦, 就是在[_strongView removeFromSuperview]后面加一句_strongView = nil;


注意weakView為nil,strongView還存在

強指針View置空

下面圖解一下, 為什么不置空, strongView就不被釋放


強弱指針View實際區別

總結起來的話, 其實如果理解到位,使用強弱都沒有問題,但是一般來說,由於弱引用會被及時的釋放掉,所以需求允許的話,一般建議使用弱引用,那什么情況不能使用弱引用呢,這個要看具體需求,舉個例子,如果一個View,需要從父View移除掉,但是之后還有可能加回來,還要保持移除之前的樣子,這種情況強引用會更適合。


收集了一些意見:

  1. 懶加載不一定一定要, 這是完全沒問題的,有人習慣把創建View,屬性賦值,添加到父視圖的代碼寫在一起,認為這樣便於管理,順着看更清晰,沒問題。
  2. 如果View層次復雜,用懶加載弱引用View的時候注意層級關系,如果理解不到位,容易產生問題,因為addSubView寫在懶加載里,極易造成層次不清晰,這時就要個人理解,用自己認為最適合的方法了。
  3. 我為什么喜歡將UI寫成懶加載?aView就是aView,bLabel就是bLabel,每個控件做好自己事情,給自己顏色字號都弄好了,等我要用你的時候,比如要往父視圖添加了,拿來直接加就好,所以在添加View這個方法里都是添加,無關代碼沒有,要修改aView背景色,去找aView(aView的懶加載里面改)啊



文/CoderLXWang(簡書作者)
原文鏈接:http://www.jianshu.com/p/bb021d4acc35
著作權歸作者所有,轉載請聯系作者獲得授權,並標注“簡書作者”。
 
 

上篇發出之后收集了一些反饋, 總結起來以下幾點:

  1. 沒有demo,代碼沒有全部粘上來。
    我認為這篇的內容不需要,不能我寫個弱引用的懶加載怎么寫,還一定要把聲明weak屬性的代碼放上來吧。
  2. 關於xib/strotyboard和純代碼
    我是兩種用過,實際工作中也基本都達到了熟練的程度,現在使用純代碼也不是公司要求,自己覺得更好,其他的就不說了,這就相當於兩條路都行的通,任意一條走成老司機了另一條也沒問題,但是新人工作中最好純代碼為主。
  3. 關於懶加載是否一定要
    理解到位想怎么搞怎么搞,蘿卜青菜各有所愛。

提綱:上篇說到第4條 1. 關於xib/storyboard 與 純代碼的對比 2. 一條規范(又提了一點關於命名的) 3. UI工廠類 與 代碼塊 4. 懶加載, View使用strong還是weak 5. 復雜界面要會分區,要會障眼法 6. masonry均布View,及其布局時約束依賴關系 7. 關於屏幕適配的一點技巧 8. 循環引用(上篇文章有人對循環引用不理解,雖然是基礎,有人不理解還是說一下吧)
5. 復雜界面要會分區,要會用障眼法

分區:什么叫分區,其實就是封裝,干啥其實都是一樣,UI網絡邏輯思路有相同的地方,就包括收拾東西,為啥很多人喜歡把各種東西用各種盒子裝起來,假設現在要寫一個aView,上面是這樣的


一個View上的元素

這個要怎么寫, 直接挨個創建直接往aView上加嗎, 這以后維護起來改點東西相信你死的心都會有的,一般這種元素有點多的都要適當的分一下區


分區后

這樣分區后,如圖所示,寫UI的時候就先依次單獨解決好上中下三部分,然后需要做的就是對上中下三部分的整體進行布局,這一級布局的時候就完全可以忽略他們內部的東西是什么樣的,全部完成整體微調,該調里面就里面, 該調整體就整體

障眼法: 所謂障眼法就是投機取巧,當然可以有各種各樣的方法,把一些復雜功能簡化,不管用了什么方法,最終看起來像是實現了就可以。
下面舉個例子, 這個例子是項目中的一個界面, 我簡化了一下抽出來, 這是一個消費記錄的界面, 有個tableview,每個cell如下所示,可以展開收起


展開狀態

收起狀態

看到這樣一個界面,首先不要考慮如何展開收起,就看一下展開的要怎么寫,(演示Demo中的UI因為沒有使用網絡數據,也為了演示方便,做了簡化,實際賬單消費下面還有一部分如何消費,可獲得什么等等的區域),參照上一條,這種一個View里元素較多的時候可以先分區如下:


按功能或者位置分區如圖

先假設展開狀態已經寫好了,下面要考慮如何收起,觀察UI發現收起狀態的信息是展開狀態中的主要信息, 如圖元素其實表達的是同樣信息


Paste_Image.png

那么難道要打破布局,將這幾個view找到重新布局,其他的隱藏掉嗎?那再點擊回到展開狀態怎么辦,在重新布局?想想就麻煩

所以,這時再搞一個summaryView,負責收起的信息展示,這個View內部的時間桌號等控件,跟展開狀態的時間桌號雖然長的一樣,但是實際是兩個不同的UI對象。


summaryView

所以完成之后,這個View里會有如下幾大塊

  • summaryView (收起的View)
  • expendBgView(展開時的整體View)
    • topView (這樣命名不好)
    • midView

如此布局,在點擊了View要展開/收起的時候,只需要轉換summaryView和expendBgView的隱藏狀態,改變一下最外層View的底部約束即可

demo地址:https://github.com/CoderLXWang/LayoutViewDemo

6. masonry均布View,及其布局時約束依賴關系

均布View: 等間距布局 - 從0開始說一下masonry的使用

約束依賴關系:這個標題其實比較寬泛,也說不好,如何寫約束本身就是比較靈活的,每個人的寫法可能都不一樣,下面舉兩個例子大概說一下,
示例1:


示例1

這個很簡單, 左右間距都是30,第二三四行View的左右約束該怎么寫,都寫下面的嗎?這樣寫如果要改這個30,就瞎了

make.left.equalTo(父視圖).offset(30); make.right.equalTo(父視圖).offset(-30);

因為這里的設計就是左右都要對其,所以下面都都依靠第一個布局即可,第二行兩個不是左右都對其

make.left.right.equalTo(父視圖).offset(第一個View);

代碼少了一行是其次,主要是改的話只改一個,也可以透過代碼看到這個地方的設計
注:這個示例很簡單,勿噴,主要說這種做法,復雜布局也需要考慮到底依靠那個View布局,具體體況多體會,簡單說就是要選取合適的依賴對象

示例2:
需求:
1.整體居中
2.寬度可變,看文字是否夠一行,最寬左右內邊距10
3.內部兩個View的centerY對其
4.最小高度為圖片高度,文字高度超度圖片,就以文字高度為准


 

 

 

直接上代碼,只為說明約束,不要找別的毛病,具體自己看吧,這里Label和ImageView一定要作為一個整體(即放到同一個父視圖中),內部因為圖片相對固定,左右尺寸都不變,要先布局圖片才可以,否則Label沒有可以依賴的東西

@interface ViewController () @property (nonatomic, strong) UIView *containerView; @end @implementation ViewController - (UIView *)containerView { if (!_containerView) { _containerView = [[UIView alloc] init]; _containerView.backgroundColor = [UIColor orangeColor]; UIImageView *imgView = [[UIImageView alloc] init]; imgView.image = [UIImage imageNamed:@"demo1.jpeg"]; [_containerView addSubview:imgView]; [imgView mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(_containerView).offset(0); make.size.mas_equalTo(CGSizeMake(100, 100)); make.centerY.equalTo(_containerView); }]; UILabel *label = [[UILabel alloc] init]; label.numberOfLines = 0; label.text = @"這是阿三沖擊紅進口付出dsk紅進口付出ds口付出dsk紅進口付出dskjfhks口付kjfhks口付出dskj紅進口付出dskjfhks口付出dskjjfhks口付出dskjfhd付出dsk紅進口付出ds口付出dsk紅進口付出dskjfhks口付kjfhks口付出dskj紅進口付出dskjfhks口付出dskjjfhks口付出dskjfhdjfhdksjhfdk"; [_containerView addSubview:label]; [label mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(imgView.mas_left).offset(-20); make.left.equalTo(_containerView).offset(0); make.bottom.top.equalTo(_containerView).offset(0); make.height.mas_greaterThanOrEqualTo(100); }]; } return _containerView; } - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.containerView]; [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view).offset(200); make.centerX.equalTo(self.view); make.width.mas_lessThanOrEqualTo([UIScreen mainScreen].bounds.size.width-20); }]; }
7. 關於屏幕適配的一點技巧

首先說一個屏幕適配到底是什么, 工作中很多人,甚至產品都搞錯了
所謂屏幕適配,並不是大屏就要將UI變大,而是要顯示更多的內容。
再說一個關於按鈕的寫UI原則
按鈕設計的大沒啥可說,如果設計的按鈕很小,到程序員手里一定要讓它看起來小,點起來大
KRATE :當然在這一基本原則下,有的時候大屏上的某些元素和小屏保持同樣大小會有一些難看,這時還是要分別對待,如果以5s屏幕尺寸為基准(也有用6的尺寸做基准的,都一樣,習慣問題),這里一般會定義這樣一個宏

#define KRATE (SCREEN_WIDTH/320.0)

舉個例子


5s效果

不做比例的適配,在6p上如圖


6p效果

其實也沒啥問題,看起來也沒有很不協調的地方,但是注意看一下券左右兩條豎直虛線,會發現大屏上左面券命和右面打印的寬度會比較小,中間區域顯得過大,應該稍微勻一點給左右兩邊,兩邊看起來會不那么擠,同時右側點擊范圍也會相應放大,做法就是將左右約束的值*KRATE

[self.leftLine mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.bgView).offset(80*KRATE); make.top.equalTo(self.bgView).offset(15); make.bottom.equalTo(self.bgView).offset(-15); make.width.mas_equalTo(1); }]; [self.rightLine mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.bgView).offset(-40*KRATE); make.top.bottom.equalTo(self.bgView).offset(0); make.width.mas_equalTo(1); }];

乘上比例之后,效果如圖


*KRATE后的6P效果

為什么會有這種問題產生, 其實也是由於個人的布局習慣引起的,在布局這個UI時,我是先將左右兩條虛線定位好,內部的東西根據虛線的位置確定,而虛線的位置就是一塊白色背景左右給定值布局的,所以換到大屏會左右寬度不變,這種情況乘個KRATE就可以了。
麻煩有些人不要來噴啥你寫就不會有這個問題,按比例分區怎樣的,那是你的方法,你要是按比例分區不也要想到底給0.幾的比例嗎,要是豎直方向在scollView里也有這個問題呢

KKRATE:因為KRATE是用不同屏幕的寬度算出的一個系數,假設某個寬度5s上寬度為10,KRATE后6上則為10375/320=11.7, 也就是屏幕大1號之后原本為10的寬度增大了1.7, 那么這個寬度如果是40呢,40*374/320 = 46.9。
為了解決小寬度 ×KRATE基本沒效果或者寬度大 ×KRATE 又過分了的問題,又定義了這樣一個宏,給KRATE在乘一個自己制定的系數,感覺沒效果就給KKRATE傳個大於1的系數,感覺過分了就KKRATE(0.95),這里注意傳入的系數小於0.86就反而大屏UI更小了,這里不想在里面繼續寫個三目運算符判斷了,就這樣了

/** 在屏幕比例基礎上再次比例, 大於0.86, 否則反而變小 */ #define KKRATE(rate) (KRATE > 1 ? KRATE*rate : KRATE)

拿一個界面舉個例子,如圖


5s效果

這里左右看起來窄窄的間距用的都是6dp,6dp如果直接*KRATE基本沒用,乘完也就加一個dp,效果基本就是如下,屏幕很大,間距很小氣,也許你會說小屏上也小氣,設計說了,你不懂,正好


6P效果

如果將各處左右間距設置為

make.left.equalTo(ws.view).offset(6*KKRATE(1.8)); make.right.equalTo(ws.view).offset(-6*KKRATE(1.8));

效果如下


*KKRATE后6P效果

明顯大氣了許多。。。

8. 循環引用(上篇文章有人對循環引用不理解,雖然是基礎,有人不理解還是說一下吧)

這部分是計划外的,因為上篇有不少同學問起這個東西,發現不少人對看似簡單的循環引用概念還是比較模糊,所以我就拿出來說一下,我會分別解釋一下常見的循環引用,以及代理,block中的循環引用問題,這里只做理解解釋,沒有深入研究,大神直接略過吧。
先說一下內存管理,大家都知道內存管理在MRC下要手動寫retain,release等代碼,操作一個對象的引用計數,以此控制對象持有及釋放,ARC下編譯器會自動添加retain/release等代碼,ARC的一個基本規則就是,只要某個對象被任一strong指針指向,那么它將不會被銷毀。如果對象沒有被任何strong指針指向,那么就將被銷毀。
所以當前我們的代碼基本都是ARC,當我們研究一個對象是否循環引用時,也就不考去考慮計數到底為幾,什么時候retain,什么時候release,我們只需要按照ARC的基本原則關心指向這個對象的strong指針。
下面就按這個基本原則解釋一下循環引用,觀察是否釋放在控制器打印dealloc方法即可
示例1:簡單粗暴無邏輯演示
有一個控制器SampleRetainCycleController *retainVCretainVC.view上面有個SampleRetainCycleView *testViewtestView有個強引用指針,指向retainVC,看起來貌似循環成一個圈了,這就是循環引用嗎?貌似怪怪的,因為少了一個引用


簡單的循環引用?

實際上少了一個引用關系,沒有考慮retainVC是那里來的,retainVC被創建之后加載nav導航棧里是被navController強引用的,這是我們就可以按ARC的原則分析了,就是看線,找實線,這里我們關心的是控制器會不會被正常釋放,那我們就看控制器有幾根實線,這時就會發現有兩根,pop出去的時候,上面的那條nav的線斷了,但是還有一條View的線,所以控制器就不會被釋放


實際循環引用圖示1

示例2:代理為什么用weak聲明
如圖就是代理為什么用弱引用,如果用強引用就變成示例1的情況了


代理用弱引用原因

示例3:一般使用block為什么注意循環引用,使用weakSelf
先說為什么block一定要用copy,既然會循環引用,那么就像代理一樣,使用弱引用的指針不行嗎?
詳細看這篇文章吧 Block為什么使用copy修飾
更詳細可以看這篇談Objective-C block的實現
總結起來就是為了使其存放在堆中,如果不copy一下,block是存放在棧中的,出了創建它的作用域,就可能被釋放掉,但是用了copy,對這個block就是強引用,所以需要注意循環引用,使用weakSelf。
那什么是weakSelf,block默認對內部引用的外部變量是強引用,所以如果直接使用了self,則相當於block有一條實線(強指針)指向self,則self又有兩條實線了


block為什么需要weakSelf

示例4:什么樣的block不會造成循環引用
最常見的就是系統的一些block與masonry,系統的比如:

    [UIView animateWithDuration: animations:^{ }];

首先是self(假設是當前的控制器)並沒有copy(強引用)這個block,其次這還是個類方法,類方法里不能對屬性進行復制,即也不能強引用這個block,所以直接用self(即block對self強引用)也不會形成循環引用

再比如masonry

[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { }];

看一下mas_makeConstraints是怎么寫的

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }

只有第三行執行了一下這個block,並沒有任何引用的代碼(即類似self.xxblock = block),所以根據上面的幾篇文章,這種block是存放在棧上的,出了作用域(即這個方法)就會被釋放掉,既然block都被釋放掉了,自然不會循環引用。

demo地址:https://github.com/CoderLXWang/RetainCycleDemo

上篇地址:關於如何寫UI及屏幕適配的一些技巧(上)



文/CoderLXWang(簡書作者)
原文鏈接:http://www.jianshu.com/p/0c1d76e7ea1a
著作權歸作者所有,轉載請聯系作者獲得授權,並標注“簡書作者”。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM