native, react native, flutter app开发框架对比

659 阅读7分钟

前端开发是离用户最近的工程领域,需要在开发时间和体验上不断作出选择和权衡, 就像著名的论断 “php是最好的计算机语言"一样, js也能依靠(node, react native)一统天下. 我们都想要一个统一的框架搞定一切.

而目前的情况是即便是同一个app的界面, 我们也在糅合这些不同的框架, 用来快速迭代,适应变化。最近抽了点时间把app开发领域人气比较高的框架凑到了一块而,对比体会了一下,其间也有一些小的收获。

缘起reactive

首先对于这些框架解决的根本问题, 我特意查了下他们官网的简介, flutter我想将它称之为reactive app, 加上react native和native 从三者的名字上看就知到reactive有多重要了. 这些年reactive面向数据流的开发趋向一直是主流, 而react 开发思想最早是从microsoft的c#语言中涌现出来的, dart和typescript是后面我将会使用的2种语言, 它们都借鉴了c#语言的优秀设计思想.

废话不多说, 这次实验实现了实时搜索并动态展示图片的简单单页面app, 先看下这次实现的app在平台上最终的效果, 没有UI设计,没有适配(轻喷)

release版本实现情况

解释下这个单页面app实现的原则, 在使用reactive开发模式的前提下, 除了必须的平台上基本的http框架,本次实现尽量或者避免使用第三方库.

native平台

首先是native 平台, kotlin实现, 使用了recyclerview+okhttp, 没有用到任何图片加载框架

虽然recyclerview为我们的gridview item提供了回收机制, 图片和view的绑定及其生命周期的处理都需要格外注意:

        (search as EditText).addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                val url = "https://anime-pictures.net/pictures/view_posts/0?&lang=zh_CN&search_tag=${s.toString()}"
                client.newCall(Request.Builder().get().url(url).build())
                    .enqueue(object: Callback {
                        override fun onFailure(call: Call, e: IOException) {
                            e.printStackTrace()
                        }
                        override fun onResponse(call: Call, response: Response) {
                            var html = response.body?.string()
                            println(html)
                            imageList.clear()
                            imageList.addAll(Regex("img_cp[\\s\\S]+?src=\"//([\\s\\S]+?)\"").findAll(
                                html.toString()
                            )
                                .map {
                                    it.groupValues[1]
                                }
                                .toList())
                            println(imageList)
                            runOnUiThread { grid.adapter?.notifyDataSetChanged() }
                        }
                    })
            }

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            }
        })
    }

    class GalleryAdapter(
        private val imageList: LinkedList<String>,
        private val client: OkHttpClient
    ) : RecyclerView.Adapter<GalleryAdapter.GalleryViewHolder>() {
        private var loadings: HashMap<String, Bitmap?> = HashMap()

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryViewHolder {
            val item : View = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
            registerAdapterDataObserver(object: AdapterDataObserver(){
                override fun onChanged() {
                    loadings = HashMap()
                }
            })
            return GalleryViewHolder(item)
}
            val url = imageList[position]
            holder.itemView.image.tag = url
            val imageRef : WeakReference<ImageView> = WeakReference(holder.itemView.image)
            if(loadings[url] != null){
                holder.itemView.image.setImageBitmap(loadings[url])
            }else {
                client.newCall(Request.Builder().get().url("https://$url").build())
                    .enqueue(object : Callback {
                        override fun onFailure(call: Call, e: IOException) {
                            e.printStackTrace()
                        }

                        override fun onResponse(call: Call, response: Response) {
                            val image = imageRef.get()
                            println("done:$url")
                            if(image != null && image.tag == url) {
                                val input: InputStream? = response.body?.byteStream()
                                if(input != null) {
                                    val bitmap: Bitmap =
                                        BitmapFactory.decodeStream(input)
                                    input.closeQuietly()
                                    loadings[url] = bitmap
                                    holder.itemView.post {
                                        holder.itemView.image.setImageBitmap(bitmap)
                                    }
                                }
                            }else if(image != null && image.tag != url){
                                //imageview reused
                                if(loadings[image.tag] != null){
                                    holder.itemView.post {
                                        holder.itemView.image.setImageBitmap(loadings[image.tag])
                                    }
                                }
                            }
                        }
                    })

这里面展示了其中整个app的核心代码逻辑, 由于没有使用图片加载框架, bitmap存到了一个临时的hashmap中和url绑定, 由于设置图片时时完全异步的, 需要对imageView的回收和复用状态做出判断,这里使用了weakreference和image.tag数据绑定作判断。要写一个做的好的话还要实现threadpool task cancel和memory diskcache等操作, 这里略去n行代码。 native app实现大约140行,如果引入glide等图片框架代码减少到120行左右,但如果加上layout xml代码40行,考虑都是可见即所得的设计算作10行kotlin的话计作 150行, native不出意料应该代码最多,且往后看。

flutter框架

flutter平台的app实现最终如下,

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'reactive'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  List<String> images = [];
  int loadings = 0;
  DateTime nows = DateTime.now();
  get serviceList => () {
        return images.map((url) => watch_image(url)).toList();
      };

  Image watch_image(String url) {
    NetworkImage image = NetworkImage("https://" + url);

    image
        .resolve(new ImageConfiguration())
        .addListener(ImageStreamListener(_handleResolve));
    return Image(
      image: image,
      height: 150.0,
    );
  }

  void _handleResolve(ImageInfo image, bool synchronousCall) {
    print(loadings);
    if (mounted && loadings > 0) {
      loadings = loadings - 1;
      if (loadings == 0) {
        print("addPostFrameCallback be invoke:" +
            (DateTime.now().second - nows.second).toString());
      }
    }
  }

  get onchange => (str) {
        http
            .get(
                "https://anime-pictures.net/pictures/view_posts/0?order_by=views&ldate=0&lang=zh_CN&search_tag=" +
                    str) //TODO
            .then((response) {
          return RegExp(r'img_cp[\s\S]+?src="\/\/([\s\S]+?)"', multiLine: true)
              .allMatches(response.body)
              .map((mt) => mt.group(1))
              .toList();
        }).then((List<String> urls) {
          print(urls);
          _updateWheel(urls);
        });
      };

  void _updateWheel(List<String> urls) {
    setState(() {
      images.clear();
      images.addAll(urls);
      loadings = urls.length;

      nows = DateTime.now();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Center(
                child: TextField(
              autofocus: true,
              onChanged: onchange,
              maxLines: 1,
            )),
            Flexible(
              flex: 1,
              child: GridView.count(
                  crossAxisCount: 2,
                  padding: EdgeInsets.symmetric(vertical: 0),
                  children: serviceList()),
            )
          ]),
    );
  }
}

    需要注意的时, 由于是实时搜索动态刷新, 需要用到stateful widget,   flutter提供的 networkimage 控件相当实用, 既简化了native中图片加载的各种缓存,又默认了最佳的display options实现起来相当高效,  其正则表达式的语法和kotlin非常相像,这里抓取的是[https://anime-pictures.net](https://anime-pictures.net/)主页的动漫图片, 正则规定了首尾的token, 最终截取group index是1的字符串.  Flutter框架整个app算上import 总共110行左右, 真实体验是代码少了,自己需要考虑的逻辑也少了不少。

react native框架

最后是最让大家感觉是个谜团的框架, 从实现机制上来说,reactnative还是需要把js的绘制指令翻译成native对应的指令才可以实现逻辑, 而flutter的reactive app是生成了gpu和cpu相关的机器绘制指令,在ui渲染这块应该很占优势才对, 下面。。。

import React, {Component} from 'react';
import {FlatList, StyleSheet, Text, View, Image, TextInput} from 'react-native';

export default function App() {
    return (<SearchGridComponent/>)
}

interface IProps {
  url: String
}

class SearchGridComponent extends Component{
    constructor(props) {
        super(props);
        this.state = {
            dataArray: []
        }
    }

    onChangeText = (text) => {
        fetch("https://anime-pictures.net/pictures/view_posts/0?order_by=views&ldate=0&lang=zh_CN&search_tag=" + text,
            {
                method: "get",
                headers: {
                    "mode": "no-cors"
                }
            })
            .then((response) => response.text())
            .then((text) => {
                console.log("seach result:" + text)
                var items = RegExp("img_cp[\\s\\S]+?src=\"//([\\s\\S]+?)\"", "gi")
                    [Symbol.match](text)
                    .flatMap((item) =>
                    {
                        return {url: "https:" + (item.substring(item.indexOf("//"), item.lastIndexOf("\"")))}
                    })
                this.setState({dataArray:items})
            })
            .catch((error) => {
                console.warn(error);
            });
    }

    render(){
        console.log(this.state["dataArray"])
        return (
            <View>
                <Text style={styles.texts}>React Native</Text>
                <TextInput onChangeText={this.onChangeText}
                        style={styles.inputs}/>
                <FlatList
                    data={this.state["dataArray"]}
                    extraData={this.state}
                    numColumns ={2}
                    keyExtractor={(item, index) => item.url}
                    renderItem={({item})=><ImageList url={item.url}/>}
                />
            </View>
        )
    }

}

class ImageList extends Component<IProps>{
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <Image source={{uri:this.props.url}} style={styles.img}/>
        )
    }
}

const styles = StyleSheet.create({
    container: {
        flexDirection: 'row',
        padding: 5,
    },
    texts:{
        width: "100%",
        height: 48,
        textAlign:'center',
        alignItems:'center',
        justifyContent:'center',
        textAlignVertical:'center',
    },
    inputs:{
        width: "100%",
        height: 40,
        backgroundColor: "#f1f2f3"
  },
  img: {
    width: "50%",
    height: 180,
  },
})

刚好到100行! 我没有特意优化什么, 都是用react的思想去实现动态交互式布局的, 如果用js实现应该比这个还少,向component的props typescript还需要定义接口, 不过这个也让代码自动化和补全有了想象空间, 论ui实现的开发效率, typescript/js 略胜flutter一筹, 10%吧 ;)其实,不过这里我要吐槽一下:

  • 环境配置方面:

reactnative 对我这个js生手来说语法啥的到容易,环境搭建,兼容性问题委实费了我一些功夫, 首先是js debug时的跨域问题, cross-origin( 这个是浏览器对运行在其页面的js限制, 在真机上debug时还是需要开代理才能绕过)

  • 框架文档一致性:

然后是reactnative expo插件, 这个在debug时是没事的, 我用的官方给的示例demo, 最后原生编译时androidx的库都找不到, 最终是通过jetifier代码反射,修改解决的, npm生态下的安装包和插件也是够全的, 官方demo更新不积极, 但是community的热情还是不错的, 对比而言flutter在文档上比reactnative厉害一些。

  • 框架api方面:

这个我没有深入去研究, 其中一个比较坑的地方就是flat component更新界面需要state + extraData同时设置才可以, 另外js的正则还是很弱, 没有extract group的功能,这里只能字符串处理来解决。虽然作为js的超集,typescript/js的语言函数api虽然繁多,但是设计和实用性还是有待考量的。

  • 历史遗留问题:

不同平台底层兼容性和相对为人诟病的内存处理机制(特别是复杂项目)

各方面对比一览:

1.开发效率/代码行数:

native(kotlin): flutter: reactnative(typescript) >150: ~110: <100

2.文档一致性(严谨自洽):

native(kotlin)≥ flutter > reactnative(typescript)

3.平均使用内存: 由于每个平台对图片作了自己的缓存和优化策略, 所以直接比较不合理, 我们从系统资源使用的角度去考量用户的UI使用体验, 这里考虑平均使用内存:

平均内存占用

native(2.8) << flutter(9.1) ~≥ reactnative(8.8) 4. 使用体验,性能方面 由于动图太大,就不上了。 每个网络加载和缓存默认策略不同, reactnative默认加载平滑一些, flutter默认图片加载的先后顺序有些慢, native默认网络加载偏慢,这块可以有极大的优化空间.

总结

综合以上各方面,以上只是实验性的检验, 具体机器上可能会有差异, 在具体开发的时候需要根据具体场景做出判断, native在资源使用上还是有无可比拟的优势, 开发效率上就另说了。 reactnative坑多,但是开发总体简单, 在追赶reactnative开发体验的同时, flutter平台对硬件性能的压榨又更进一步了。