iOS vs Flutter(語法篇)

iOS開發者入門Flutter

首先說一下,爲什麼要關心iOS和Flutter的區別問題。因爲移動端開發的業務邏輯設計模式等是一致的,區別可能只在於使用的語言不同,實現邏輯的風格不同而已。所以這裏我們先分析一下iOS和Flutter的區別到底有哪些,有利於我們更快地去入門。

生命週期:

頁面加載的生命週期:

移動端開發首先要關注的一點肯定要了解一個頁面加載的生命週期,就像瞭解iOS的viewcontroller的生命週期在UI繪製的場景中有多麼重要:

iOS:

iOS的設計初衷就是MVC,所以iOS中的核心是Controller。每個Controller都有自己的生命週期:

//類的初始化方法
+ (void)initialize;
//對象初始化方法
- (instancetype)init;
//從歸檔初始化
- (instancetype)initWithCoder:(NSCoder *)coder;
//加載視圖
-(void)loadView;
//將要加載視圖
- (void)viewDidLoad;
//將要佈局子視圖
-(void)viewWillLayoutSubviews;
//已經佈局子視圖
-(void)viewDidLayoutSubviews;
//內存警告
- (void)didReceiveMemoryWarning;
//已經展示
-(void)viewDidAppear:(BOOL)animated;
//將要展示
-(void)viewWillAppear:(BOOL)animated;
//將要消失
-(void)viewWillDisappear:(BOOL)animated;
//已經消失
-(void)viewDidDisappear:(BOOL)animated;
//被釋放
-(void)dealloc;
flutter:

這一點flutter則有所不同,flutter頁面加載的核心則是build一棵渲染樹的過程。

在這裏插入圖片描述

如圖所示大體上它的生命週期可以分爲三個過程:初始化,狀態改變,銷燬。每一次build都是頁面的一次加載。

didUpdateWidget: Flutter中的Widget分爲兩種:stateful(狀態可變)和stateless(狀態不可變)。如果是stateful的widget需要實現setState方法。當調用了 setState 將 Widget 的狀態被改變時 didUpdateWidget 會被調用,Flutter 會創建一個新的 Widget 來綁定這個 State,並在這個方法中傳遞舊的 Widget ,因此如果你想比對新舊 Widget 並且對 State 做一些調整,你可以用它,另外如果你的某些 Widget 上涉及到 controller 的變更,要麼一定要在這個回調方法中移除舊的 controller 並創建新的 controller 監聽。

dispose:某些情況下你的 Widget 被釋放了,一個很經典的例子是 Navigator.pop 被調用,如果被釋放的 Widget 中有一些監聽或持久化的變量,你需要在 dispose 中進行釋放。很重要的一個應用是在 Bloc 或 Stream 時在這個回調方法中去關閉 Stream。

可參考博客:https://segmentfault.com/a/1190000015211309

App 的生命週期

iOS中的APP的生命週期在appdelegate裏設置,這裏不多說。

而在flutter中如果我們想監聽 App 級別的生命週期,可以通過向Binding 中添加一個􏵞􏴥􏰲􏱀 Observer,同時要實現didChangeAppLifecycleState來監聽指定事件的到來,並且最後還需要在 dispose 回調方法中移除這個監聽。但限制是它在iOS平臺智能監聽三種狀態。

class LifeCycleDemoState extends State<MyHomePage> with WidgetsBindingObserver {
     @override
     void initState() {
       super.initState();
       WidgetsBinding.instance.addObserver(this);
     }
     @override
     void didChangeAppLifecycleState(AppLifecycleState state) {
       super.didChangeAppLifecycleState(state);
       switch (state) {
         case AppLifecycleState.inactive:
           print('Application Lifecycle inactive');
           break;
         case AppLifecycleState.paused:
           print('Application Lifecycle paused');
           break;
         case AppLifecycleState.resumed:
           print('Application Lifecycle resumed');
           break;
         default:
           print('Application Lifecycle other');
       }
		}
    @override
    void dispose(){
      WidgetsBinding.instance.removeObserver(this);
      super.dispose();
    }
}

詳細使用源碼看這裏:https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/binding.dart

佈局:

Flutter的佈局首先要建立一棵Widget樹(分析一下UI圖建立一棵控件樹),根據樹的結構層層嵌套Widget控件。margin和padding等的約束佈局設置則有點類似css。

UIView => Widgets

iOS:UIView可變,UIView發生改變本質上是間接調用(官方建議不要顯式調用,因爲這開銷很大)了LayoutSubviews方法進行佈局上的重構,drawRect進行顯示上的重構,updateConstrains進行約束上的重構。三者的更新會在每次RunLoop之後進行update cycle操作(也可以調用layoutIfNeeded等方法進行立刻更新)。(推薦一篇講述佈局不錯的文章:https://juejin.im/post/5a951c655188257a804abf94)

Flutter:Widget不可變,也正是由於不可變性,使得Widget比起UIView來說更輕量,因爲它不是控件,只是UI的描述。

  • widget的分爲Stateful(狀態可變)和Stateless(狀態不可變)兩種,Stateful本質上也是不可變的,它只是將狀態拆分到State中去管理。State會存儲Widget的狀態數據,並在widget樹重建時攜帶着它,因此狀態不會丟失。
  • 即使一個widget是有狀態的,包含它的父親的widget也可以是無狀態的,只要父widget本身不響應這些變化即可。
  • 在UI更新這一點上感覺flutter會比iOS要麻煩一點,例如點擊按鈕導致Text的文字發生變化,iOS只需要簡單的target-action就可以,而flutter則需要將無狀態的text套在一個StatefulWidget父類裏面纔可以。
class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text
  String textToShow = "Flutter";
  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Text is changed!";
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("My App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        child: Icon(Icons.update),
      ),
    );
  }
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(300, 600, 80, 40);
    [button setTitle:@"點擊" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 300, 200, 60)];
    self.textField.text = @"iOS";
    [self.view addSubview:self.textField];
}

-(void) onClick
{
    self.textField.text = @"text has changed!";
}
  • 在佈局方面,iOS分爲xib和storyboard還有代碼佈局的方式,flutter比較統一,通過建立一棵widget樹進行佈局,同時還有類似Center,Column等Widget進行佈局。添加約束的話,iOS通過autolayout來添加約束,而flutter則是通過使用container容器添加padding,margin等屬性來添加約束。這點非常類似html和css的佈局原理。如下所示:

在這裏插入圖片描述

  • 父子視圖的移除方面,比如說舉個常見的例子,一個bool值的改變導致兩個視圖的切換,iOS需要在父view中調用addSubview()或在子view中調用removeFromSuperView() 來動態添加或移除子view,如果涉及到一些傳值問題可能還需要通過觀察者模式來觀察bool值的更新。而在flutter中則需要向父widget中傳入一個返回widget的函數,並用bool來控制子widget的創建。

    class SampleApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Sample App',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: SampleAppPage(),
        );
      }
    }
    
    class SampleAppPage extends StatefulWidget {
      SampleAppPage({Key key}) : super(key: key);
    
      @override
      _SampleAppPageState createState() => _SampleAppPageState();
    }
    
    class _SampleAppPageState extends State<SampleAppPage> {
      bool toggle = true;
      void _toggle() {
        setState(() {
          toggle = !toggle;
        });
      }
    
      _getToggleChild() {
        if (toggle) {
          return Text('View One');
        } else {
          return CupertinoButton(
            onPressed: () {},
            child: Text('View Two'),
          );
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("Sample App"),
          ),
          body: Center(
            child: _getToggleChild(),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _toggle,
            tooltip: 'Update Text',
            child: Icon(Icons.update),
          ),
        );
      }
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
        button.frame = CGRectMake(350, 750, 50, 30);
        [button setTitle:@"切換" forState:UIControlStateNormal];
        [button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
        self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 300, 200, 60)];
        self.textField.text = @"View 1";
        [self.view addSubview: self.textField];
        self.button2 = [UIButton buttonWithType:UIButtonTypeSystem];
        self.button2.frame = CGRectMake(100, 300, 200, 60);
        [self.button2 setTitle:@"View 2" forState:UIControlStateNormal];
    }
    
    -(void) onClick
    {
        if (self.textField.superview) {
            [self.textField removeFromSuperview];
            [self.view addSubview:self.button2];
        }
        else
        {
            [self.button2 removeFromSuperview];
            [self.view addSubview:self.textField];
        }
    
    }
    

導航

UIViewController => Scaffold

Flutter中沒有專門用來管理視圖而且類似那種和View一對一的Controller類。有類似的Scaffold,其包含控制器的appBar,也可以通過body設置一個widget來做其視圖。

頁面跳轉

iOS有UINavigationController棧進行push,pop操作,其並不負責顯示,而是負責各個頁面跳轉。或者使用模態視圖。

同時注意iOS通過navigationController導航時要記得添加NavigationController,可在stroyboard設置,如果代碼的話添加如下:

ViewController *vc = [ViewController new];
UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:vc];
self.window.rootViewController = navigation;
[self.window makeKeyWindow];
// --------  ViewController  --------
-(NextViewController *)next
{
    if (!_next)
    {
        _next = [NextViewController new];
    }
    return _next;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(350, 750, 50, 30);
    [button setTitle:@"切換" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

-(void) onClick
{
    [self.navigationController pushViewController:self.next animated:YES];
    // [self presentViewController:self.next animated:YES completion:nil];
}
// --------  NextViewController  --------
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // Do any additional setup after loading the view.
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(350, 750, 50, 30);
    [button setTitle:@"返回" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

-(void) onClick
{
    [self.navigationController popViewControllerAnimated:YES];
    // [self dismissViewControllerAnimated:YES completion:nil];
}

Flutter中可以將MaterialApp理解爲iOS的導航控制器,其包含一個navigationBar以及導航棧,這點和iOS是一樣的。

Flutter中使用了 NavigatorRoutes。一個路由是 App 中“屏幕”或“頁面”的抽象,而一個 Navigator 是管理多個路由的 widget。可以粗略地把一個路由對應到一個 UIViewController。Navigator 的工作原理和 iOS 中 UINavigationController 非常相似,當你想跳轉到新頁面或者從新頁面返回時,它可以 push()pop() 路由。

在頁面之間跳轉,有如下選擇:

  • 具體指定一個由路由名構成的 Map。(MaterialApp)
  • 直接跳轉到一個路由。(WidgetApp)

下面是構建一個 Map 的例子:

void main() {
  runApp(MaterialApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

通過把路由的名字 push 給一個 Navigator 來跳轉:

Navigator.of(context).pushNamed('/b');

頁面傳值

iOS中頁面傳值正向直接通過屬性傳輸即可,反向的話可以通過Block,delegate,通知等方式進行傳值。

而在Flutter中反向傳值就簡單了很多,舉個例子,要跳轉到“位置”路由來讓用戶選擇一個地點,可能要這麼做:

Map coordinates = await Navigator.of(context).pushNamed('/location');

之後,在 location 路由中,一旦用戶選擇了地點,攜帶結果一起 pop() 出棧:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

多線程

RunLoop VS EventLoop

說起Flutter的多線程前先談一下Flutter中的Event Loop。我們都知道前端開發框架大都是事件驅動的。意味着程序中必然存在事件循環和事件隊列。事件循環會不斷地從事件隊列中獲取和處理各種事件。

iOS:iOS中的類似機制是RunLoop,可以通過一個RunLoopObserver來實現對RunLoop的觀察,當遇到Source0,Source1或者Timer等事件會進行處理。下面圖可以很清晰的理解整個過程。(關於RunLoop的講解推薦一篇很詳細的文章:https://juejin.im/post/5add46606fb9a07abf721d1d)

在這裏插入圖片描述

Flutter:Flutter中的Event Loop和JavaScript的基本一樣。循環中有兩個隊列。一個是微任務隊列(MicroTask queue),一個是事件隊列(Event queue)。這裏類似iOS中存在set裏面的Source0和Source1。

  • event queue: 主要是外部事件,負責處理I/O事件、繪製事件、手勢事件、Timer等
  • microtask queue:可以自己向isolate內部添加事件,事件的優先級比event queue高。

在這裏插入圖片描述

兩個隊列是有優先級的,當isolate開始執行後,會先處理microtask的事件,當microtask隊列中沒有事件後,纔會處理event隊列中的事件,並按照這個順序反覆執行。當執行microtask的事件時會阻塞event隊列,這會導致渲染響應手勢等event事件響應延遲。爲保證渲染和手勢響應,所以應儘量將耗時操作放到event隊列中。

線程和協程:

Flutter中的多線程和異步是比較難理解的一塊。先看兩個語法糖:

  1. Future:

    學過js的童鞋應該很容易理解這塊了,Future就是js裏的Promise,曾經js裏面延時只能層層回調,這樣很容易造成回調地獄。所以應運而生了Promise(Future),它是一個鏈式操作,可以通過追加then方法進行多層處理。

  2. async/await:用法完全沿用自js。

多協程:

我們拆開來看,首先對於異步操作這塊,flutter基本上是沿用ES6的async和await這些異步語法糖以及Future。這裏涉及到一個和傳統意義不一樣的概念——協程(https://www.itcodemonkey.com/article/4620.html這篇漫畫講的比較生動,https://www.zhihu.com/question/308641794講解爲什麼協程比線程要好),最早接觸是在python裏有遇到過,在Flutter中,執行到async則表示進入一個協程,會同步執行async的代碼塊。當執行到await時,則表示有任務需要等待,CPU則去調度執行其他IO。過一段時間CPU會輪詢一次查看某個協程是否任務已經處理完成,有返回結果可以被繼續執行,如果可以被繼續執行的話,則會沿着上次離開時指針指向的位置繼續執行。也就是await標誌的位置。

iOS中有沒有類似的實現呢,其實是有的,個人感覺是串行隊列中的dispatch_async操作,遇到耗時操作也不會阻塞,而是放到事件隊列中等待當前操作執行完再繼續執行。Flutter在當執行到await時,保存當前的上下文,並將當前位置標記爲待處理任務,用一個指針指向當前位置,並將待處理任務放入當前線程的隊列中。在每個事件循環時都去詢問這個任務,如果需要進行處理,就恢復上下文進行任務處理。

多線程:

而async和await是沿用自js的,但有一點要注意的是,js是腳本語言,所以必須是單線程的,到這裏就足夠了。而flutter是手機框架,很可能我們要進行很耗時的IO操作,這僅僅憑異步是解決不了的,必須要多線程。這裏flutter引入了新的方案,叫做isolate。

但isolate和普通的Thread還不同,它具有獨立的內存,isolate間的通信通過port來實現,這個port消息傳遞的過程是異步的。實例化一個isolate的過程也就是實例化isolate這個結構體、在堆中分配線程內存,配置port。

感覺從操作來看其實isolate更像是進程,而實際async則更像是線程操作。

loadData() async {
    // 通過spawn新建一個isolate,並綁定靜態方法
    ReceivePort receivePort =ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);
    
    // 獲取新isolate的監聽port
    SendPort sendPort = await receivePort.first;
    // 調用sendReceive自定義方法
    List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
    print('dataList $dataList');
}

// isolate的綁定方法
static dataLoader(SendPort sendPort) async{
    // 創建監聽port,並將sendPort傳給外界用來調用
    ReceivePort receivePort =ReceivePort();
    sendPort.send(receivePort.sendPort);
    
    // 監聽外界調用
    await for (var msg in receivePort) {
      String requestURL =msg[0];
      SendPort callbackPort =msg[1];
    
      Client client = Client();
      Response response = await client.get(requestURL);
      List dataList = json.decode(response.body);
      // 回調返回值給調用者
      callbackPort.send(dataList);
    }    
}

// 創建自己的監聽port,並且向新isolate發送消息
Future sendReceive(SendPort sendPort, String url) {
    ReceivePort receivePort =ReceivePort();
    sendPort.send([url, receivePort.sendPort]);
    // 接收到返回值,返回給調用者
    return receivePort.first;
}

(代碼引用自https://lequ7.com/2019/04/26/richang/shen-ru-li-jie-Flutter-duo-xian-cheng/)

網絡請求和數據解析

這點不做詳述,理解了Dart的多線程以及網絡請求的基本原理也很容易搞懂這塊(https://flutterchina.club/networking/)。

總結

文章不從性能進行分析,而是僅僅從語法方面進行入門比對。其實如果做過Web前端的童鞋學期Flutter來說應該會比較簡單,因爲Flutter中的Widget佈局方式完全類似於html和css。而Dart語言本身有非常像js,尤其是ES6特性引入很多語法糖後的js,這些語法糖大大簡化了代碼的複雜度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章