22、Flutter - 混合开发(三)iOS原生调用Flutter_ios 原生主动调用flutter_shengdaVolleyball的博客-程序员宝宝

技术标签: Flutter  

混合开发(三)iOS原生调用Flutter


Flutter 项目 调用一些原生的功能!用的比较多的就是第三方插件,因为比较简单

官方 《Flutter实战》

原生项目中部分页面使用Flutter,这种也是比较常见的。

FLutter本身定位的是开发一个完整的App应用。所以要是只让其做成一个页面的话有些功能是不支持的。Flutter本身有自己的渲染引擎,如果是小项目用Flutter就不划算,只有非常大型的项目将其部分或者全部页面用Flutter来实现。

 

详细代码参见Demo

Demo地址 -> AiOSFlutterModule

 

1、FLutter Module

模块创建

创建出来的工程看一下

打开文件路径可以去看一下,是隐藏文件

隐藏的目的是,官方不希望我们对这些文件进行操作。我们开发的是一个Flutter页面是能运行的,这里的ios 和 Android 文件只是为了让我们做测试用的。当我们把这个Flutter写好之后,是要集成到原生项目中使用的,并不需要那些隐藏的内容。

扩展:

打开终端输入命令:(由于系统不一样,有可能无效,可以自行上网查阅)

1、显示隐藏文件/文件夹

$ defaults write com.apple.finder AppleShowAllFiles -boolean true ; killall Finder

2、隐藏隐藏文件/文件夹

$ defaults write com.apple.finder AppleShowAllFiles -boolean false ; killall Finder

 

2、新建iOS项目

要让Flutter和我们的iOS项目产生管理,使用pod进行管理
先生成Podfiile 文件,直接打开终端,cd 打开iOS项目路径。pod init ,pod install。之后就可以再Xcode 里面查看了。

[email protected] ~ % cd /Users/liujilou/Desktop/code/AiOSFlutterModule/NativeDemo
[email protected] NativeDemo % pod init
[email protected] NativeDemo % pod install

然后用Xcode 打开工程,编辑Podfile 文件。这里的引用格式的Flutter官网提供的,可以去查阅

flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

platform :ios, '9.0'

target 'NativeDemo' do
  install_all_flutter_pods(flutter_application_path)
  use_frameworks!
 
end

注意:这个Flutter 的路径是相对路径,如果修改改了路径,这里记得要改变


重新 pod install



这样就关联成功了,一些 Flutter 的内容就安装进去了
这个时候我们也可以到ViewController 里面试一下是否成功了。
#import <Flutter/Flutter.h> 头文件能导入就说明成功了

 

3、调用 Flutter 页面(一)不推荐

iOS里面先创建2个按钮,然后去打开Flutter页面

3.1、iOS页面

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    CGSize viewSize = self.view.frame.size;
    
    UIButton * button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button1.frame = CGRectMake((viewSize.width-100)/2, 100, 100, 40);
    button1.backgroundColor = [UIColor orangeColor];
    button1.tag = 1001;
    [button1 setTitle:@"按钮一" forState:(UIControlStateNormal)];
    [button1 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
    [self.view addSubview:button1];
    
    
    UIButton * button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button2.frame = CGRectMake((viewSize.width-100)/2, 200, 100, 40);
    button2.backgroundColor = [UIColor greenColor];
    button2.tag = 1002;
    [button2 setTitle:@"按钮二" forState:(UIControlStateNormal)];
    [button2 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
    [self.view addSubview:button2];
    
}

-(void)pushFlutter:(UIButton *)btn
{
    NSString * pageIndex = @"one";
    NSString * page = @"one_page";
    if (btn.tag == 1002) {
        pageIndex = @"two";
        page = @"two_page";
    }
//    不要每次都去alloc init 一个新的FLutter,这样非常消耗性能
    FlutterViewController * vc = [[FlutterViewController alloc] init];
    [vc setInitialRoute:pageIndex];//初始化传给Flutter的值
    [self presentViewController:vc animated:YES completion:nil];
}

我们这里用的是跟iOS一样的调用方式,每次点击按钮的时候去重新创建Flutter页面。
如果全屏显示Flutter页面,在Flutter中要做回退。我们先不做全屏显示,先演示后面做

3.2、Flutter的页面

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp(
    //window 需要导入import 'dart:ui';
    //window.defaultRouteName 拿到的就是 iOS中写的 [vc setInitialRoute:@"one"];带过来的值 one
    pageIndex: window.defaultRouteName));

// ----------------------------------------------------
class MyApp extends StatelessWidget {
  final String pageIndex;

  const MyApp({Key key, this.pageIndex}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: rootPage(pageIndex),
    );
  }

  rootPage(String pageIndex) {
    switch (pageIndex) {
      case 'one':
        return Scaffold(
            appBar: AppBar(title: Text(pageIndex)),
            body: Center(
                child: RaisedButton(
              onPressed: () {
//              直接退出页面了,一般不会这么做。
                MethodChannel('one_page').invokeMapMethod('exit');
              },
              child: Text(pageIndex),
            )));
      case 'two':
        return Scaffold(
          appBar: AppBar(title: Text(pageIndex)),
          body: Center(
              child: RaisedButton(
            onPressed: () {
              MethodChannel('two_page').invokeMapMethod('exit');
            },
            child: Text(pageIndex),
          )),
        );
    }
  }
}
  • iOS通过初始化的时候传值给Flutter   [vc setInitialRoute:pageIndex];
  • Flutter通过  window.defaultRouteName  拿到iOS传过来的值,然后去判断 显示AppBar 等。然后 可以通过  MethodChannel('one_page').invokeMapMethod('exit');   退出Flutter页面,并给iOS发送消息传值。

 

但是Flutter 创建之后是一直存在在内存中的,而且非常大

通过上图可以看到,一个空的Flutter都非常大。而且每次创建的话Flutter页面的数据不能保存。因为Flutter要有自己的渲染引擎,不能像iOS的页面一样这样创建,所以建议全局建一个 Flutter 引擎。

 

4、Flutter 页面调用(二)推荐

@property (nonatomic, strong) FlutterEngine * flutterEngine;
@property (nonatomic, strong) FlutterViewController * flutterVC;

Flutter 引擎

- (FlutterEngine *)flutterEngine
{
    if (!_flutterEngine) {
        //这里不要直接用 _flutterEngine ,然后 _flutterEngine.run 因为在页面将要显示的时候才去执行运行,那么Flutter的页面显示的会非常慢
        //_flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
        //定义一个局部变量,判断一下如果这个flutterEngine已经运行起来了,那么我们的全局_flutterEngine就等于这个 flutterEngine 。失败的话就返回nil
        
        FlutterEngine * flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
        if (flutterEngine.run) {//Flutter 运行运行起来了
            _flutterEngine = flutterEngine;
        }
    }
    return _flutterEngine;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
    self.flutterVC.modalPresentationStyle = UIModalPresentationFullScreen;//模态展示风格(全屏显示)
-(void)pushFlutter:(UIButton *)btn
{
    NSString * pageIndex = @"one";
    NSString * page = @"one_page";
    if (btn.tag == 1002) {
        pageIndex = @"two";
        page = @"two_page";
    }

    [self presentViewController:self.flutterVC animated:YES completion:nil];
}

4.1、问题

//    直接这样写就会出错,崩溃.
window.defaultRouteName 是空的
为什么是空的呢?因为在run的时候就去运行了Flutter,但是这一个时候并没有去传值。
可以通过单独运行Flutter 代码看一下值,和报错信息

调试

 

因为  [vc setInitialRoute:pageIndex]; 传值是在初始化的时候传的,我们现在不每次都去创建Flutter了所以需要改一下。Flutter也不能通过  window.defaultRouteName 来取值了。

修改Flutter代码

4.2、Flutter

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

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

// ----------------------------------------
class _MyApp extends StatefulWidget {
  @override
  __MyAppState createState() => __MyAppState();
}

class __MyAppState extends State<_MyApp> {
  String pageIndex = 'one';

//  这里是解码器,互相调用
  final MethodChannel _oneChannel = MethodChannel('one_page');
  final MethodChannel _twoChannel = MethodChannel('two_page');

  @override
  void initState() {
    // 调用方法一次接收信息
    _oneChannel.setMethodCallHandler((call) {
      setState(() {
        pageIndex = call.method;
      });
      return null;
    });

    _twoChannel.setMethodCallHandler((call) {
      setState(() {
        pageIndex = call.method;
      });
      return null;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: rootPage(pageIndex),
    );
  }

  rootPage(String pageIndex) {
    switch (pageIndex) {
      case 'one':
        return Scaffold(
            appBar: AppBar(title: Text(pageIndex)),
            body: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                  onPressed: () {
//              直接退出页面了,一般不会这么做。
                    MethodChannel('one_page').invokeMapMethod('exit');
                  },
                  child: Text(pageIndex),
                ),
              ],
            ));
      case 'two':
        return Scaffold(
          appBar: AppBar(title: Text(pageIndex)),
          body: Center(
              child: RaisedButton(
            onPressed: () {
              MethodChannel('two_page').invokeMapMethod('exit');
            },
            child: Text(pageIndex),
          )),
        );
    }
  }
}

 

5、通讯

1、FlutterMethodChannel   调用方法(method invocation)一次通讯


//下面的这里两种都是持续通讯的
2、 FlutterBasicMessageChannel    :传递字符串&半结构化的信息
3、FlutterEventChannel   :用于数据流(stream)的通讯

完整的代码

5.1、iOS

//  ViewController.m
//  NativeDemo
//
//  Created by liujilou on 2020/6/28.
//  Copyright  2020 liujilou. All rights reserved.
//

    
//    FlutterMethodChannel   调用方法(method invocation)一次通讯
//下面的这里两种都是持续通讯的
//    FlutterBasicMessageChannel    :传递字符串&半结构化的信息
//    FlutterEventChannel   :用于数据流(stream)的通讯

#import "ViewController.h"
#import <Flutter/Flutter.h>

@interface ViewController ()

@property (nonatomic, strong) FlutterEngine * flutterEngine;
@property (nonatomic, strong) FlutterViewController * flutterVC;
@property (nonatomic, strong) FlutterBasicMessageChannel * msgChannel;

@end

@implementation ViewController

- (FlutterEngine *)flutterEngine
{
    if (!_flutterEngine) {
        //这里不要直接用 _flutterEngine ,然后 _flutterEngine.run 因为在页面将要显示的时候才去执行运行,那么Flutter的页面显示的会非常慢
        //_flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
        //定义一个局部变量,判断一下如果这个flutterEngine已经运行起来了,那么我们的全局_flutterEngine就等于这个 flutterEngine 。失败的话就返回nil
        
        FlutterEngine * flutterEngine = [[FlutterEngine alloc] initWithName:@"liujilou"];
        if (flutterEngine.run) {//Flutter 运行运行起来了
            _flutterEngine = flutterEngine;
        }
    }
    return _flutterEngine;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
    self.flutterVC.modalPresentationStyle = UIModalPresentationFullScreen;//模态展示风格(全屏显示)
    
//    接收Flutter 的数据
//   这里因为messenger 需要 FlutterBinaryMessenger 类型所以报警告
    self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVC];
    
    [self.msgChannel setMessageHandler:^(id  _Nullable message, FlutterReply  _Nonnull callback) {
        NSLog(@"收到Flutter 的%@",message);
    }];
    
    
    CGSize viewSize = self.view.frame.size;
    
    UIButton * button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button1.frame = CGRectMake((viewSize.width-100)/2, 100, 100, 40);
    button1.backgroundColor = [UIColor orangeColor];
    button1.tag = 1001;
    [button1 setTitle:@"按钮一" forState:(UIControlStateNormal)];
    [button1 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
    [self.view addSubview:button1];
    
    
    UIButton * button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button2.frame = CGRectMake((viewSize.width-100)/2, 200, 100, 40);
    button2.backgroundColor = [UIColor greenColor];
    button2.tag = 1002;
    [button2 setTitle:@"按钮二" forState:(UIControlStateNormal)];
    [button2 addTarget:self action:@selector(pushFlutter:) forControlEvents:(UIControlEventTouchUpInside)];
    [self.view addSubview:button2];
    
}


-(void)pushFlutter:(UIButton *)btn
{
    NSString * pageIndex = @"one";
    NSString * page = @"one_page";
    if (btn.tag == 1002) {
        pageIndex = @"two";
        page = @"two_page";
    }
//    不要每次都去alloc init 一个新的FLutter,这样非常消耗性能
//    FlutterViewController * vc = [[FlutterViewController alloc] init];
    
    
//    直接这样写就会出错,奔溃.
//    window.defaultRouteName 是空的
//    FlutterViewController * vc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
//
//    vc.modalPresentationStyle = UIModalPresentationFullScreen;//模态跳转页面全屏显示
    
//    问题就出来 Route,Route在run之后,所以Flutter收不到初始化的数据,这里就不用Route 了。
//    因为Route本身就是在初始化的时候传值用的,我们既然不想让每次都去重新创建,那就不会每次都初始化所以这里用Route 就不合适了
//    [vc setInitialRoute:pageIndex];//初始化的时候带过去的值
    
//    [self presentViewController:vc animated:YES completion:nil];
    
//    使用Channel 通道传值
    FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:page binaryMessenger:self.flutterVC];
//    发送消息
    [methodChannel invokeMethod:pageIndex arguments:nil];
    
    [self presentViewController:self.flutterVC animated:YES completion:nil];
    
//   监听 Flutter 回调回来的参数
    [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        if ([call.method isEqualToString:@"exit"]) {
            [self.flutterVC dismissViewControllerAnimated:YES completion:nil];
        }
    }];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int a = 0;
    [self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
}

@end

做持续通讯

iOS点击屏幕,就向Flutter发送消息。将a 值传过去

 

5.2、Flutter

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(_MyApp()
//    MyApp(
//    window 需要导入import 'dart:ui';
//  window.defaultRouteName 拿到的就是 iOS中写的 [vc setInitialRoute:@"one"];带过来的值 one
//    pageIndex: window.defaultRouteName
//    )
    );

// ----------------------------------------------------
class MyApp extends StatelessWidget {
  final String pageIndex;

  const MyApp({Key key, this.pageIndex}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: rootPage(pageIndex),
    );
  }

  rootPage(String pageIndex) {
    switch (pageIndex) {
      case 'one':
        return Scaffold(
            appBar: AppBar(title: Text(pageIndex)),
            body: Center(
                child: RaisedButton(
              onPressed: () {
//              直接退出页面了,一般不会这么做。
                MethodChannel('one_page').invokeMapMethod('exit');
              },
              child: Text(pageIndex),
            )));
      case 'two':
        return Scaffold(
          appBar: AppBar(title: Text(pageIndex)),
          body: Center(
              child: RaisedButton(
            onPressed: () {
              MethodChannel('two_page').invokeMapMethod('exit');
            },
            child: Text(pageIndex),
          )),
        );
    }
  }
}

// ----------------------------------------
class _MyApp extends StatefulWidget {
  @override
  __MyAppState createState() => __MyAppState();
}

class __MyAppState extends State<_MyApp> {
  String pageIndex = 'one';

//  这里是解码器,互相调用
  final MethodChannel _oneChannel = MethodChannel('one_page');
  final MethodChannel _twoChannel = MethodChannel('two_page');
//  这个是通讯,也需要一个解码器
  final BasicMessageChannel _messageChannel =
      BasicMessageChannel('messageChannel', StandardMessageCodec());

  @override
  void initState() {
//    可以持续接收信息
    _messageChannel.setMessageHandler((message) {
      print('收到了来自iOS的:$message');
      return null;
    });

    // 调用方法一次接收信息
    _oneChannel.setMethodCallHandler((call) {
      setState(() {
        pageIndex = call.method;
      });
      return null;
    });

    _twoChannel.setMethodCallHandler((call) {
      setState(() {
        pageIndex = call.method;
      });
      return null;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: rootPage(pageIndex),
    );
  }

  rootPage(String pageIndex) {
    switch (pageIndex) {
      case 'one':
        return Scaffold(
            appBar: AppBar(title: Text(pageIndex)),
            body: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                  onPressed: () {
//              直接退出页面了,一般不会这么做。
                    MethodChannel('one_page').invokeMapMethod('exit');
                  },
                  child: Text(pageIndex),
                ),
                TextField(
//                  输入框写数据,向iOS发送数据
                  onChanged: (String str) {
                    _messageChannel.send(str);
                  },
                )
              ],
            ));
      case 'two':
        return Scaffold(
          appBar: AppBar(title: Text(pageIndex)),
          body: Center(
              child: RaisedButton(
            onPressed: () {
              MethodChannel('two_page').invokeMapMethod('exit');
            },
            child: Text(pageIndex),
          )),
        );
    }
  }
}

 

Flutter 和原生页面不要频繁来回切换,内存消耗会非常大。
同时Flutter 销毁是不会完全销毁的,所以就不要去销毁了,就整体保存一份引擎避免重复创建。

混合开发
可以利用FLutter 作为项目的主要开发框架

也可以如上,将FLutter作为一个业务,iOS做框架。但是还是那句话不要频繁的来回切换。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/shengdaVolleyball/article/details/107063652

智能推荐

在Windows 10中使用统一写过滤器(UWF)_enhanced write filter安装_huihuiwith的博客-程序员宝宝

原文:http://woshub.com/using-unified-write-filter-uwf-windows-10/Windows 10(和Windows 8)的一个有用功能是特殊的文件系统写过滤器--UWF(统一写过滤器)。如果启用并配置了过滤器,则磁盘上的文件和目录的所有更改都将在RAM中进行,并在重新引导后重置。UWF如何运作?它通过透明地将文件系统中的所有写入操作重定向到...

Python 工匠:使用数字与字符串的技巧_Python开发者的博客-程序员宝宝

(给Python开发者加星标,提升Python技能)作者:piglei 『Python 工匠』是什么?我一直觉得编程某种意义上是一门『手艺』,因为优雅而高效的代码,就如同...

java List集合转换String,String转换List,byte转换为String的方法_『JC』的博客-程序员宝宝

list数组中的某个参数,比如:一个证书,多个管理员等需求时,可以将list进行字符串拼接,生成string字符串返给前端显示1.第一种方法采用java8 String.join 字符串拼接// 我比较喜欢这种方法public static void main(String[] args) { List&lt;String&gt; list = Lists.newArrayL...

RNA-seq分析流程_Liripo的博客-程序员宝宝

一般已发表文章所包含的数据可以在NCBI (SRA、GEO 等)、EMBL-EBI 等相关数据库获得。ncbi数据下载参考高通量测序知识下载测序数据后,可以进行质控(fastqc等),比对(bwa,STAT,subread等,非常多),获取counts数,之后差异分析,GO,KEGG等。当然还有call snp等。质控fastqc使用,相对应的R包fastqcr,rqcfastpBiostrings包计算GC含量,Q20等library(Biostrings)filepath &lt.

wvd的matlab实现程序,matlab代码实现stft_weixin_39827728的博客-程序员宝宝

文件PNCompare_Spectrum.mPM_Abs_Thre_Hearing.mPM_Simu_Masking.mPNcorrelation.mPNsequence.mPSNR_seq.mQPSK ModemDQPSKmodem.cppDQPSKreceiver.cppDQPSKtransmitter.cppDiffDetector.CPPDiffEncoder.cppEnergyDetec...

python执行js? 提示:$ is not defined_添色增香的博客-程序员宝宝

python中使用execjs执行js提示execjs._exceptions.ProgramError: ReferenceError: $ is not definedjs使用jquery写的,所以没有定义$,该怎么解决此问题?

随便推点

TencentのPlugin文件夹_weixin_30657999的博客-程序员宝宝

pluginList.db 插件列表  Com.Tencent.Advertisement 广告  Com.Tencent.AudioVideo 语音视频  Com.Tencent.Bookmark QQ书签  Com.Tencent.CRM 企业好友  Com.Tencent.FileTransfer 文件传送  Com.Tencent.GameLife 游戏人生  Com.Ten...

对象间的联动——观察者模式(一)_halcyon.fun的博客-程序员宝宝

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 观察者模式是设计模式中的“超级模式”,其应用随处可见,在之后几篇文章里,我将向大家详细介绍观察者模式。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; “...

gloox连接openfire 一直断开,错误码为 18_鱼儿-1226的博客-程序员宝宝

错误信息:ft_send: disconnected: 16ce: 18解决方案:此问题为 证书认证失败。 需要加载认证证书 即可解决问题。j-&gt;setSASLMechanisms(SaslMechPlain);

duilib-自定义曲线控件_duilib 绘制曲线_feng_blog6688的博客-程序员宝宝

duilib-自定义曲线控件duilib现有的控件继承图如下:从上图可以看出常见的控件都是由CControlUI继承而来,因此如果需要自定义控件,可以继承CControlUI,重写子类。如何做一个类似windows任务管理器的曲线控件,如下图所示:自定义曲线控件展示如下:下面详细说明如何在duilib源码中自定义曲线控件,以及如何在xml中设置控件属性。1、重写控件类CChartCtrlUI,继承于CLabelUI或者CControlUI,控件类CChartCtrlUI中必须重写的函数方法如

推荐文章

热门文章

相关标签