flutter 开发 android的完整流程

本文主要以一个简单的android应用程序的开发与发布过程对flutter进行一个比较基础的介绍。很多过程其实就是按照官方文档做的,只是中间会穿插一些遇到的问题以及解决方案。

1. 安装flutter

以macOS为例。

1.1 国内的镜像配置

flutter的sdk也好,使用过程中也好,都需要从远程下载,所以我们配置国内的源来加速这些过程。

1.1.1 配置源

根据使用的shell不同,编辑不同的rc文件即可,我使用的是zsh,所以编辑~/.zshrc文件,增加一个源的配置。下面给出国内的一些源,根据自己的速度测试情况来使用。不管选择哪个镜像,只需要把对应的代码加入到rc文件中即可。

Flutter 社区

社区主镜像,采用多种方式同步 Flutter 开发者资源(推荐)。

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

上海交大 Linux 用户组

使用反向代理方式建立的 Flutter 镜像,数据与站源实时同步。 Pub API 返回值未做处理,可能造成无法访问的情况。

export PUB_HOSTED_URL=https://dart-pub.mirrors.sjtug.sjtu.edu.cn
export FLUTTER_STORAGE_BASE_URL=https://mirrors.sjtug.sjtu.edu.cn

清华大学 TUNA 协会

定时与 Flutter 社区 Storage 镜像同步,Pub API 采取定时主动抓取策略,镜像配置了完善的失败回源策略(推荐)。

export PUB_HOSTED_URL=https://mirrors.tuna.tsinghua.edu.cn/dart-pub
export FLUTTER_STORAGE_BASE_URL=https://mirrors.tuna.tsinghua.edu.cn/flutter

CNNIC

基于 TUNA 协会的镜像服务,数据策略与 TUNA 一致,通过非教育网的域名访问。

export PUB_HOSTED_URL=http://mirrors.cnnic.cn/dart-pub
export FLUTTER_STORAGE_BASE_URL=http://mirrors.cnnic.cn/flutter

腾讯云开源镜像站

定时(每天凌晨)与 TUNA 协会镜像同步,数据有延迟,访问速度有待反馈。

export PUB_HOSTED_URL=https://mirrors.cloud.tencent.com/dart-pub
export FLUTTER_STORAGE_BASE_URL=https://mirrors.cloud.tencent.com/flutter

1.1.2 下载sdk

从国内下载sdk,只需要从上述镜像中选择一个,然后访问FLUTTER_STORAGE_BASE_URL对应的地址加/flutter_infra/releases/目录去下载即可。

编写本文时,使用的版本是flutter_macos_v1.12.13+hotfix.5-stable, 已经上传到百度云了: https://pan.baidu.com/s/1jwEjfzJAaUUrFVHPFZOkog

1.2 解压并设置环境变量

我的sdk下载位置是 ~/Downloads/flutter_macos_v1.12.13+hotfix.5-stable.zip ,sdk准备解压到的位置是 ~/codes/private/flutter-projects/sdks, 所以在命令行执行命令 :

cd  ~/codes/private/flutter-projects/sdks
unzip ~/Downloads/flutter_macos_v1.12.13+hotfix.5-stable.zip

命令完成后,我们在rc文件里增加flutter相关的命令到环境变量:

export PATH=$PATH:/Users/z/codes/private/flutter-projects/sdks/flutter/bin/

保存rc文件后,执行source ~/.zshrc使环境变量对当前终端生效。

1.3 运行flutter doctor检查环境是否正常

在命令行执行flutter doctor命令,如果正常的话,会有类似输出:

  ╔════════════════════════════════════════════════════════════════════════════╗
  ║                 Welcome to Flutter! - https://flutter.dev                  ║
  ║                                                                            ║
  ║ The Flutter tool uses Google Analytics to anonymously report feature usage ║
  ║ statistics and basic crash reports. This data is used to help improve      ║
  ║ Flutter tools over time.                                                   ║
  ║                                                                            ║
  ║ Flutter tool analytics are not sent on the very first run. To disable      ║
  ║ reporting, type 'flutter config --no-analytics'. To display the current    ║
  ║ setting, type 'flutter config'. If you opt out of analytics, an opt-out    ║
  ║ event will be sent, and then no further information will be sent by the    ║
  ║ Flutter tool.                                                              ║
  ║                                                                            ║
  ║ By downloading the Flutter SDK, you agree to the Google Terms of Service.  ║
  ║ Note: The Google Privacy Policy describes how data is handled in this      ║
  ║ service.                                                                   ║
  ║                                                                            ║
  ║ Moreover, Flutter includes the Dart SDK, which may send usage metrics and  ║
  ║ crash reports to Google.                                                   ║
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
  ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting        ║
  ║                                                                            ║
  ║ See Google's privacy policy:                                               ║
  ║ https://www.google.com/intl/en/policies/privacy/                           ║
  ╚════════════════════════════════════════════════════════════════════════════╝


Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.15.2 19C57, locale zh-Hans-CN)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.dev/setup/#android-setup for detailed instructions).
      If the Android SDK has been installed to a custom location, set ANDROID_HOME to that location.
      You may also want to add it to your PATH environment variable.

[✗] Xcode - develop for iOS and macOS
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
        sudo xcodebuild -runFirstLaunch
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To install:
        sudo gem install cocoapods
[!] Android Studio (not installed)
[!] VS Code (version 1.41.1)
    ✗ Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
[!] Connected device
    ! No devices available

! Doctor found issues in 5 categories.

里面的问题我们慢慢解决,只要看到类似输出,我们前面的步骤就算完成了。

1.4 安装android开发环境

flutter需要完整的Android Studio开发环境以提供全部的依赖支持。

1.4.1 安装android studio

a. 下载并安装Android Studio
b. 启动Android Studio, 然后通过 ‘Android Studio Setup Wizard’安装最新的 Android SDK, Android SDK Platform-Tools, 以及 Android SDK Build-Tools, 他们都是 Flutter开发 Android所需要的。
c. 打开avd编辑器,创建一个虚拟设备,如果不知道步骤,可以参考如下内容:

  1. Enable VM acceleration on your machine.
  2. Launch Android Studio > Tools > Android > AVD Manager and select Create Virtual Device. (The Android submenu is only present when inside an Android project.)
  3. Choose a device definition and select Next.
  4. Select one or more system images for the Android versions you want to > emulate, and select Next. An x86 or x86_64 image is recommended.
  5. Under Emulated Performance, select Hardware - GLES 2.0 to enable hardware acceleration.
  6. Verify the AVD configuration is correct, and select Finish.
    For details on the above steps, see Managing AVDs.
  7. In Android Virtual Device Manager, click Run in the toolbar. The emulator starts up and displays the default canvas for your selected OS version and device.

1.5 安装代码开发工具

1.5.1 VS Code

下载vscode,然后搜索flutter以及dart的扩展进行安装即可,安装后可能需要重启vscode。


安装后,cmd+shift+p,然后输入Run Flutter Doctor验证安装是否正确。如果vscode找不到sdk,那么会弹出错误,选择手动指定我们之前下载的flutter sdk的路径即可。

2. 编写代码

2.1 创建工程

在vscode里面,cmd+shift+p,然后输入Flutter New Project,然后根据提示输入名称创建一个project,创建成功后,会直接打开main.dart。

image.png

2.2 启动工程

保持编辑器打开main.dart的状态,切换到debug页签点击debug flutter app即可启动调试。
如果卡在Running gradle assembleDebug,那应该是因为gradle源的问题,我们可以修改到阿里云的源。
a. 修改项目中android/build.gradle文件,把两个repository配置修改一下

buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        // google()
        // jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        // google()
        // jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
    }
}

rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
    project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

b. 修改Flutter的配置文件, 该文件在Flutter安装目录/packages/flutter_tools/gradle/flutter.gradle

buildscript {
    repositories {
        //修改的地方
        //google()
        //jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
    }
}

我在启动的时候,还遇到了一个奇怪的问题,一直卡在Installing build/app/outputs/apk/app.apk...,关掉调试工具,在控制台输入flutter run --verbose后,成功运行了app,之后再退出这个命令,回到vscode中打开调试,就正常了。

image.png

2.3 hot-reload

修改main.dart,把文字改成你已经点击了这么多次按钮,保存文件,更新直接会同步到设备上:

image.png

2.4 编写一个入门程序

2.4.1 编写一个hello world

替换main.dart为一下内容:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'hello world',
      home: Scaffold(
        appBar: AppBar(
          title: Text('标题'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

如果服务还开着的话,模拟器应该直接会变成如下界面


2.4.2 添加依赖的使用

依赖的添加是在pubspec.yamldependencies里面编辑的,我们打开刚才项目的pubspec.yaml增加一行对english_words的依赖。 更多的依赖可以到dart的依赖库里面找 https://pub.dev/


如果服务还在运行的话,应该会发现控制台自动执行了flutter pub get来安装我们最新声明的依赖。如果没有,那么就手动执行这个命令即可。

然后我们修改main.dart如下:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordPair = WordPair.random();
    return MaterialApp(
      title: 'hello world',
      home: Scaffold(
        appBar: AppBar(
          title: Text('标题'),
        ),
        body: Center(
          child: Text(wordPair.asPascalCase),
        ),
      ),
    );
  }
}

如果一切正常的话,应该就可以看到如下的一个随机名字的输出了:


image.png

2.4.3 一个名字列表的app

我们写一个稍微复杂一点的app:写一个列表,列表可以无限滚动,边滚动,边加载新的随机字符串,同时列表每个元素中间都用分割线分开。
完整代码如下:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      home: RandomWords(),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  RandomWordsState createState() => RandomWordsState();
}

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];  /* 存放内容的列表 */
  final _biggerFont = const TextStyle(fontSize: 18.0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
        // itemCount: 20,  /* 如果是有限列表,那么通过指定itemCount即可设置列表元素数量。 */
        padding: const EdgeInsets.all(2.0),
        itemBuilder: /*itemBuilder是渲染列表元素的方法。 ListView在滚动的时候或者初始化的时候,会自动计算需要获取哪些位置的数据,然后通过调用这个方法来获取内容*/
         (context, i) {
          if (i.isOdd) return Divider(); /*如果是奇数,就渲染一个分割线*/

          final index = i ~/ 2; /*~/运算是除去2之后的整数部分*/
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10)); /*如果现在要取的列表的数据下标已经超过了已有元素的个数,就再往list里面增加10个数据*/
          }
          return _buildRow(_suggestions[index]); /* 渲染一行数据 */
        });
  }

  Widget _buildRow(WordPair pair) {
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }
}

细节在代码里面都注释了,这里再解释一下整体的思路:
我们修改myapp的build方法,直接渲染RandomWords组件,然后RandomWords组件的build方法里面会渲染Scaffold组件,然后在body中调用_buildSuggestions方法来渲染ListView。RandomWordsState里面使用_suggestions列表存放数据内容,使用_biggerFont存放字体样式。_buildSuggestions方法里面编写了ListView的渲染逻辑(如果是奇数就渲染分割线,如果是偶数就渲染数据元素,数据元素的渲染由_buildRow方法提供。 如果发现渲染元素的时候,元素不够用,那么就加载十个元素到_suggestions列表中,然后把对应位置的数据进行渲染)

3 构建程序

3.1 检查 App Manifest

查看默认应用程序清单文件(位于<app dir>/android/app/src/main/中的AndroidManifest.xml文件),并验证这些值是否正确,特别是:

  • application: 编辑 application 标签, 这是应用的名称。

  • uses-permission: 如果您的应用程序代码不需要Internet访问,请删除android.permission.INTERNET权限。标准模板包含此标记是为了启用Flutter工具和正在运行的应用程序之间的通信。

3.2 查看构建配置

查看”build.gradle”,它位于<app dir>/android/app/,验证这些值是否正确,尤其是:

  • defaultConfig:

    • applicationId: 指定始终唯一的 (Application Id)appid

    • versionCode & versionName: 指定应用程序版本号和版本号字符串。有关详细信息,请参考版本文档

    • minSdkVersion & targetSdkVersion: 指定最低的API级别以及应用程序设计运行的API级别。有关详细信息,请参阅版本文档中的API级别部分。

3.3 添加启动图标

当一个新的Flutter应用程序被创建时,它有一个默认的启动器图标。要自定义此图标:

  1. 查看Android启动图标 设计指南,然后创建图标。

  2. <app dir>/android/app/src/main/res/目录中,将图标文件放入使用配置限定符命名的文件夹中。默认mipmap-文件夹演示正确的命名约定。

  3. AndroidManifest.xml中,将application标记的android:icon属性更新为引用上一步中的图标(例如 <application android:icon="@mipmap/ic_launcher" ...)。

  4. 要验证图标是否已被替换,请运行您的应用程序并检查应用图标

3.4 app签名

3.4.1 创建 keystore

如果您有现有keystore,请跳至下一步。如果没有,请通过在运行以下命令来创建一个: keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

注意:保持文件私密; 不要将它加入到公共源代码控制中。

注意: keytool可能不在你的系统路径中。它是Java JDK的一部分,它是作为Android Studio的一部分安装的。有关具体路径,请百度。

3.4.2 引用应用程序中的keystore

创建一个名为<app dir>/android/key.properties的文件,其中包含对密钥库的引用:

storePassword=<前面创建秘钥时对应的store密码>
keyPassword=<前面创建秘钥时对应的key密码>
keyAlias=key
storeFile=<jks文件的路径,例如/Users/<user name>/key.jks>

注意: 保持文件私密; 不要将它加入公共源代码控制中

3.4.3 在gradle中配置签名

通过编辑<app dir>/android/app/build.gradle文件为您的应用配置签名

  1. 替换:

    android {
    
    

    为:

    def keystorePropertiesFile = rootProject.file("key.properties")
    def keystoreProperties = new Properties()
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
    
    android {
    
    
  2. 替换:

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
    
    

    为:

    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile file(keystoreProperties['storeFile'])
            storePassword keystoreProperties['storePassword']
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
    
    

    现在,您的应用的release版本将自动进行签名。

3.5 开启混淆

默认情况下 flutter 不会开启 Android 的混淆。

如果使用了第三方 Java 或 Android 库,也许你想减小 apk 文件的大小或者防止代码被逆向破解。

3.5.1 配置混淆

创建 /android/app/proguard-rules.pro 文件,并添加以下规则:

#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.**  { *; }
-keep class io.flutter.util.**  { *; }
-keep class io.flutter.view.**  { *; }
-keep class io.flutter.**  { *; }
-keep class io.flutter.plugins.**  { *; }

上述配置只混淆了 Flutter 引擎库,任何其他库(比如 Firebase)需要添加与之对应的规则。

3.5.2 开启混淆/压缩

打开 /android/app/build.gradle 文件,定位到 buildTypes 块。

release 配置中将 minifyEnableduseProguard 设为 true,再将混淆文件指向上一步创建的文件。

android {

    ...

    buildTypes {

        release {

            signingConfig signingConfigs.release

            minifyEnabled true
            useProguard true

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }
    }
}

3.6 解决一些问题

3.6.1 mac上的权限问题

下一步执行构建的时候,可能会报一些命令是从网上下载的,是否要删除之类的提示,这个时候我们需要通过finder定位到具体报错的文件位置,然后按住ctrl再点击文件,就会出现运行选项,点击运行后,这个文件后面就不会报错了。

3.6.2 关于android包名的问题

我们前面创建app的时候,vscode默认创建的android包名为com.example.xxxxx,如果需要修改的话,要全局搜索这个包名然后替换,有非常非常多的地方需要替换,如果替换错误,可能会导致app无法打包或者无法正确安装、运行。

3.7 构建一个发布版(release)APK

本节介绍如何构建发布版(release)APK。如果您完成了前一节中的签名步骤,则会对APK进行签名。

使用命令行:

  1. cd <app dir> (<app dir> 为您的工程目录).
  2. 运行flutter build apk (flutter build 默认会包含 --release选项).

打包好的发布APK位于<app dir>/build/app/outputs/apk/app-release.apk

3.8 在设备上安装发行版APK

按照以下步骤在已连接的Android设备上安装上一步中构建的APK

使用命令行:

  1. 用USB您的Android设备连接到您的电脑
  2. cd <app dir> .
  3. 运行 flutter install .

3.9 将APK发布到Google Play商店

将应用的release版发布到Google Play商店的详细说明,请参阅 Google Play publishing documentation. (国内不存在的,但你可以发布到国内的各种应用商店)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容