与iOS通信-libimobiledevice

1.什么是libimobiledevice?

A cross-platform software protocol library and tools to communicate with iOS® devices natively.

libimobiledevice is a cross-platform software library that talks the protocols to support iPhone®, iPod Touch®, iPad® and Apple TV® devices. Unlike other projects, it does not depend on using any existing proprietary libraries and does not require jailbreaking. It allows other software to easily access the device's filesystem, retrieve information about the device and it's internals, backup/restore the device, manage SpringBoard® icons, manage installed applications, retrieve addressbook/calendars/notes and bookmarks and (using libgpod) synchronize music and video to the device. The library is in development since August 2007 with the goal to bring support for these devices to the Linux Desktop.

官方网站:http://www.libimobiledevice.org
github地址:https://github.com/libimobiledevice

2.libimobiledevice目前的运用:

PP助手,itools, 爱思助手,xy助手等一大批苹果助手类软件,底层全部都是使用的此库。

3.快速直接安装libmobiledevice的方法

在MacOS下安装可以使用brew,类似Ubuntu中的apt-get

brew update
brew install libimobiledevice
#libimobiledevice中并不包含ipa的安装命令,所以还需要安装
brew install ideviceinstaller

Ubuntu下安装需要添加一个新的软件库,里面包含了libimobiledevice

sudo add-apt-repository ppa:pmcenery/ppa
sudo apt-get update
apt-get install libimobiledevice-utils
sudo apt-get install ideviceinstaller

常用功能

安装ipa包,卸载应用

//命令安装一个ipa文件到手机上,如果是企业签名的,非越狱机器也可以直接安装了。
ideviceinstaller -I xxx.ipa

//命令卸载应用,需要知道此应用的bundleID
ideviceinstaller -U [bundleID]

查看系统日志

idevicesyslog

查看当前已连接的设备的UUID

idevice_id --list

截图

idevicescreenshot

查看设备信息

ideviceinfo

获取设备时间

idevicedate

设置代理(可以用于转发端口,比如将ssh的端口映射到电脑,这样没有网络也可以ssh登录)

iproxy

挂载DeveloperDiskImage,用于调试

ideviceimagemounter

获取设备名称

idevicename

调试程序(需要预先挂载DeveloperImage)

idevicedebug

查看和操作设备的描述文件

ideviceprovision list

挂载文件系统工具:ifuse
ifuse是一个依赖libimobiledevice库的工具,所以必须首先安装libimobiledevice

brew install osxfuse
brew install ifuse

挂载某应用的整个沙盒目录

ifuse --container [要挂载的应用的bundleID] [挂载点]

如果是越狱的设备,并且配置好了,可以使用下面命令挂载整个iphone文件系统(暂时没试过,还没有开始研究越狱设备)

ifuse --root [挂载点]

4.手动编译

github:https://github.com/libimobiledevice/libimobiledevice
安装依赖软件和库

brew install openssl
brew install libplist
brew install usbmuxd//包含libusbmuxd
#brew install make //make不需要安装,系统自带
brew install autoconf//里面包含autoheader
brew install automake//包含automake
brew install libtool
brew install pkg-config
#brew install gcc//系统自带

生成Makefile,必须指定openssl的路径,由于openssl的漏洞,mac不在支持openssl,不支持将openssl的库加入库路径,所以必须指定路径

./autogen.sh openssl_CFLAGS=/usr/local/opt/openssl/include openssl_LIBS=/usr/local/opt/openssl/lib

编译

make

安装

sudo make install

5.什么是Makefile

Linux 环境下的程序员如果不会使用GNU make来构建和管理自己的工程,应该不能算是一个合格的专业程序员,至少不能称得上是 Unix程序员。在 Linux(unix )环境下使用GNU 的make工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入一些时间去完成一个或者多个称之为Makefile 文件的编写。
所要完成的Makefile 文件描述了整个工程的编译、连接等规则。其中包括:工程中的哪些源文件需要编译以及如何编译、需要创建那些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。尽管看起来可能是很复杂的事情,但是为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的唯一的一件事就是在shell 提示符下输入make命令。整个工程完全自动编译,极大提高了效率。
make是一个命令工具,它解释Makefile 中的指令(应该说是规则)。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。Makefile(在其它的系统上可能是另外的文件名)在绝大多数的IDE 开发环境中都在使用,已经成为一种工程的编译方法。

使用Makefile例子
1、建目录

$ mkdir helloword
$ cd helloworld

2、 hello.c

#include <stdio.h>


int main(int argc, char const *argv[])
{
    printf("hello word!\n");
    return 0;
}

3.首先使用gcc编译

gcc -o hello hello.c
./hello
output:hello word!

4.使用Makefile编译
在工作目录建立Makefile文件

hello:hello.c
    gcc -o hello hello.c

clean: 
    rm hello

使用make编译

make
output:gcc -o hello hello.c
./hello
output:hello word!

5.Autoconf和Automake使用
1.生成configure

autoscan
ls
autoscan.log    configure.scan  hello.c

2.生成configure.ac
现在将configure.scan改名为configure.ac,并且编辑它,按下面的内容修改,去掉无关的语句:

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ([2.69])
AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
AM_INIT_AUTOMAKE(hello, 1.0)
AC_CONFIG_SRCDIR([hello.c])
AC_CONFIG_HEADERS([config.h])

# Checks for programs.
AC_PROG_CC

# Checks for libraries.

# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.

AC_CONFIG_FILES([Makefile])
AC_OUTPUT

3.执行aclocal和autoconf

aclocal
ls
aclocal.m4      autom4te.cache  autoscan.log    configure.ac    configure.scan  hello.c
autoconf
ls
aclocal.m4      autom4te.cache  autoscan.log    configure       configure.ac    configure.scan  hello.c

可以看到configure.ac内容是一些宏定义,这些宏经autoconf处理后会变成检查系统特性、环境变量、软件必须的参数的shell脚本。
autoconf 是用来生成自动配置软件源代码脚本(configure)的工具。configure脚本能独立于autoconf运行,且在运行的过程中,不需要用户的干预。
要生成configure文件,你必须告诉autoconf如何找到你所用的宏。方式是使用aclocal程序来生成你的aclocal.m4。
aclocal根据configure.ac文件的内容,自动生成aclocal.m4文件。aclocal是一个perl 脚本程序,它的定义是:“aclocal - create aclocal.m4 by scanning configure.ac”。
autoconf从configure.ac这个列举编译软件时所需要各种参数的模板文件中创建configure。
autoconf需要GNU m4宏处理器来处理aclocal.m4,生成configure脚本。
m4是一个宏处理器。将输入拷贝到输出,同时将宏展开。宏可以是内嵌的,也可以是用户定义的。除了可以展开宏,m4还有一些内建的函数,用来引用文件,执行命令,整数运算,文本操作,循环等。m4既可以作为编译器的前端,也可以单独作为一个宏处理器.
4.执行autoheader

autoheader
ls
aclocal.m4      autom4te.cache  autoscan.log    config.h.in     configure       configure.ac    configure.scan  hello.c

5.新建Makefile.am

AUTOMAKE_OPTIONS=foreign
bin_PROGRAMS=hello
hello_SOURCES=hello.c 

6.运行automake

automake --add-missing
configure.ac:6: warning: AM_INIT_AUTOMAKE: two- and three-arguments forms are deprecated.  For more info, see:
configure.ac:6: https://www.gnu.org/software/automake/manual/automake.html#Modernize-AM_005fINIT_005fAUTOMAKE-invocation
configure.ac:11: installing './compile'
configure.ac:6: installing './install-sh'
configure.ac:6: installing './missing'
Makefile.am: installing './depcomp'
ls
Makefile.am     aclocal.m4      autoscan.log    config.h.in     configure.ac    depcomp         install-sh
Makefile.in     autom4te.cache  compile         configure       configure.scan  hello.c         missing

automake会根据Makefile.am文件产生一些文件,包含最重要的Makefile.in。
7.执行configure生成Makefile

./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables...
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking whether gcc understands -c and -o together... yes
checking whether make supports the include directive... yes (GNU style)
checking dependency style of gcc... gcc3
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: executing depfiles commands
ls
Makefile        aclocal.m4      compile         config.log      configure.ac    hello.c         stamp-h1
Makefile.am     autom4te.cache  config.h        config.status   configure.scan  install-sh
Makefile.in     autoscan.log    config.h.in     configure       depcomp         missing

8.make及运行

make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-am
gcc -DHAVE_CONFIG_H -I.     -g -O2 -MT hello.o -MD -MP -MF .deps/hello.Tpo -c -o hello.o hello.c
mv -f .deps/hello.Tpo .deps/hello.Po
gcc  -g -O2   -o hello hello.o
ls
Makefile        aclocal.m4      compile         config.log      configure.ac    hello           install-sh
Makefile.am     autom4te.cache  config.h        config.status   configure.scan  hello.c         missing
Makefile.in     autoscan.log    config.h.in     configure       depcomp         hello.o         stamp-h1
./hello
hello word!

autotools命令详解:
1、 autoscan
  autoscan是用来扫描源代码目录生成configure.scan文件的。autoscan可以用目录名做为参数,但如果你不使用参数的话,那么autoscan将认为使用的是当前目录。autoscan将扫描你所指定目录中的源文件,并创建configure.scan文件。

2、 configure.scan
  configure.scan包含了系统配置的基本选项,里面都是一些宏定义。我们需要将它改名为configure.ac

3、 aclocal
  aclocal是一个perl 脚本程序。aclocal根据configure.ac文件的内容,自动生成aclocal.m4文件。aclocal的定义是:“aclocal - create aclocal.m4 by scanning configure.ac”。

4、 autoconf
  使用autoconf,根据configure.in和aclocal.m4来产生configure文件。configure是一个脚本,它能设置源程序来适应各种不同的操作系统平台,并且根据不同的系统来产生合适的Makefile,从而可以使你的源代码能在不同的操作系统平台上被编译出来。
  configure.ac文件的内容是一些宏,这些宏经过autoconf 处理后会变成检查系统特性、环境变量、软件必须的参数的shell脚本。configure.ac文件中的宏的顺序并没有规定,但是你必须在所有宏的最前面和最后面分别加上AC_INIT宏和AC_OUTPUT宏。
  在configure.ini中:

  #号表示注释,这个宏后面的内容将被忽略。

  AC_INIT(FILE) //这个宏用来检查源代码所在的路径。
  AM_INIT_AUTOMAKE(PACKAGE, VERSION) //这个宏是必须的,它描述了我们将要生成的软件包的名字及其版本号:PACKAGE是软件包的名字,VERSION是版本号。当你使用make dist命令时,它会给你生成一个类似helloworld-1.0.tar.gz的软件发行包,其中就有对应的软件包的名字和版本号。
  AC_PROG_CC  //这个宏将检查系统所用的C编译器。
  AC_OUTPUT(FILE)  //这个宏是我们要输出的Makefile的名字。

我们在使用automake时,实际上还需要用到其他的一些宏,但我们可以用aclocal 来帮我们自动产生。执行aclocal后我们会得到aclocal.m4文件。   产生了configure.ac和aclocal.m4 两个宏文件后,我们就可以使用autoconf来产生configure文件了。

5、 Makefile.am
  Makefile.am是用来生成Makefile.in的,需要你手工书写。Makefile.am中定义了一些内容:

  AUTOMAKE_OPTIONS  //在执行automake时,它会检查目录下是否存在标准GNU软件包中应具备的各种文件,例如AUTHORS、ChangeLog、NEWS等文件。我们将其设置成foreign时,automake会改用一般软件包的标准来检查。
  bin_PROGRAMS  //这个是指定我们所要产生的可执行文件的文件名。如果你要产生多个可执行文件,那么在各个名字间用空格隔开。
  helloworld_SOURCES  //这个是指定产生“helloworld”时所需要的源代码。如果它用到了多个源文件,那么请使用空格符号将它们隔开。比如需要helloworld.h,helloworld.c那么请写成helloworld_SOURCES= helloworld.h helloworld.c。
  如果你在bin_PROGRAMS定义了多个可执行文件,则对应每个可执行文件都要定义相对的filename_SOURCES。

6、 automake
  使用automake,根据configure.in和Makefile.am来产生Makefile.in。
  --add-missing //定义是“add missing standard files to package”,它会让automake加入一个标准的软件包所必须的一些文件。
  用automake产生出来的Makefile.in文件是符合GNU Makefile惯例的,接下来我们只要执行configure这个shell 脚本就可以产生合适的 Makefile 文件了。

7、 Makefile
  在符合GNU Makefiel惯例的Makefile中,包含了一些基本的预先定义的操作:

  make      //根据Makefile编译源代码,连接,生成目标文件,可执行文件。
  make clean  //清除上次的make命令所产生的object文件(后缀为“.o”的文件)及可执行文件。
  make install  //将编译成功的可执行文件安装到系统目录中,一般为/usr/local/bin目录。
  make list  //产生发布软件包文件(即distribution package)。这个命令会将可执行文件及相关文件打包成一个tar.gz压缩的文件用来作为发布软件的软件包。它会在当前目录下生成一个名字类似“PACKAGE-VERSION.tar.gz”的文件。PACKAGE和VERSION,是我们在configure.ac中定义的AM_INIT_AUTOMAKE(PACKAGE, VERSION)。
  make distcheck  //生成发布软件包并对其进行测试检查,以确定发布包的正确性。这个操作将自动把压缩包文件解开,然后执行configure命令,并且执行make,来确认编译不出现错误,最后提示你软件包已经准备好,可以发布了。
  make distclean  //类似make clean,但同时也将configure生成的文件全部删除掉,包括Makefile。
667911-20160531165212977-1698924539.gif

5.代码使用libimobiledevice

//
//  main.m
//  testlibimobiledevice
//
//  Created by dujianjie on 2018/6/26.
//  Copyright © 2018年 dujianjie. All rights reserved.
//
#include <pthread.h>
#include <libimobiledevice/house_arrest.h>
#include <libimobiledevice/afc.h>
#include <libimobiledevice/lockdown.h>
#import <libimobiledevice/libimobiledevice.h>
#import <Foundation/Foundation.h>

void* modify(void *info) {
    int *error = malloc(sizeof(int));
    
    size_t len = strlen((char *)info);
    char *udid = malloc(len+1);
    if (!udid) {
        *error = -10;
        goto l_udid;
    }
    memset(udid, 0, len+1);
    memcpy(udid, info, len);
    
    idevice_t device = NULL;
    idevice_new(&device, udid);
    if (!device) {
        *error = -10;
        goto l_device;
    }
    
    NSLog(@"idevice_new success");
    
    lockdownd_client_t lockdownd_client = NULL;
    lockdownd_error_t lockdownd_err = LOCKDOWN_E_UNKNOWN_ERROR;
    
    lockdownd_err = lockdownd_client_new_with_handshake(device, &lockdownd_client, "handshake");
    if (lockdownd_err != LOCKDOWN_E_SUCCESS) {
        *error = -10;
        goto l_device;
    }
    NSLog(@"lockdownd_client_new_with_handshake success");
//    lockdownd_client_free(lockdownd_client);
    
    house_arrest_error_t err = 0;
    house_arrest_client_t client = NULL;
   
    err = house_arrest_client_start_service(device, &client, NULL);
    if(err  != HOUSE_ARREST_E_SUCCESS) {
        *error = err;
        goto l_device;
    }
    
    NSLog(@"house_arrest_client_start_service success");
    err = house_arrest_send_command(client, "VendContainer", "com.cvte.EasiAir");
    if(err  != HOUSE_ARREST_E_SUCCESS) {
        *error = err;
        goto l_house;
    }
    
    NSLog(@"house_arrest_send_command success");
    plist_t dict = NULL;
    
    err = house_arrest_get_result(client, &dict);
    
    if(err  != HOUSE_ARREST_E_SUCCESS) {
        NSLog(@"house_arrest_get_result  :%d", err);
        *error = err;
        goto l_house;
    }
    char *xml = NULL;
    uint32_t length = 0;
    plist_to_xml(dict, &xml, &length);
    
    NSLog(@"house_arrest_get_result success:%s",xml);
    if (xml) free(xml);
    
    
    plist_t node = plist_dict_get_item(dict, "Status");
    if (!node) {
        *error = -4;
        goto l_list;
    }
    char *status = NULL;
    plist_get_string_val(node, &status);
    if (!status) {
        *error = -4;
        goto l_list;
    }
    NSLog(@"status:%s", status);
    
    if (strcmp(status, "Complete")!=0) {
        *error = -4;
        goto l_status;
    }
    NSLog(@"house_arrest_send_command success");
    
    afc_client_t afc_client = NULL;
    afc_error_t afc_err = afc_client_new_from_house_arrest_client(client, &afc_client);
    
    if (afc_err != AFC_E_SUCCESS) {
        *error = afc_err;
        goto l_status;
    }
    NSLog(@"afc_client_new_from_house_arrest_client success");
    
    afc_err = afc_make_directory(afc_client, "Documents/Custom_dujj");
    
    if (afc_err != AFC_E_SUCCESS) {
        *error = afc_err;
        goto l_status;
    }
    NSLog(@"afc_make_directory success");
    
l_status:
    free(status);
l_list:
    plist_free(dict);
l_house:
     house_arrest_client_free(client);
l_device:
    idevice_free(device);
l_udid:
    free(udid);
    return error;
}

void idevice_event_callback(const idevice_event_t *event, void *user_data) {
    switch(event->event) {
        case IDEVICE_DEVICE_ADD:{
            NSLog(@"add device %s", event->udid);
            static pthread_t devmon = 0;
            if (devmon == 0)
            pthread_create(&devmon, NULL, modify, (void *)event->udid);
        }break;
        case IDEVICE_DEVICE_REMOVE:{
            NSLog(@"remove device %s", event->udid);
        }break;
        case IDEVICE_DEVICE_PAIRED:{
            NSLog(@"paired device %s", event->udid);
        }break;
        default: {
            
        }break;
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        
        idevice_set_debug_level(1);
        
        idevice_error_t err = idevice_event_subscribe(idevice_event_callback, NULL);
        struct timespec ts;
        ts.tv_sec = 180;
        ts.tv_nsec = 0;
        
        while(1) {
            nanosleep(&ts, NULL);
        }
        
        err = idevice_event_unsubscribe();
    }
    return 0;
}

6.libimobiledevice内部魔法

1.usbmuxd
A socket daemon to multiplex connections from and to iOS devices


1.png

2.守护进程
守护进程时一个在后台运行并且不受任何终端控制的进程。
百度百科

3.udev
Linux kernel2.6系列的设备管理器,在libimobiledevice中用于监控usb热插拔。
百度百科

4.systemd
Systemd 是 Linux 系统中最新的初始化系统(init),它主要的设计目标是克服 sysvinit 固有的缺点,提高系统的启动速度。
百度百科

7.libimobiledevice结构图

2.png

8.libimobiledevice提供的iOS服务

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

推荐阅读更多精彩内容