• [原]百度公交离线数据格式分析——3.加载城市列表


    1. 在进入OfflineDataManageActivity时,找到 onCreate() 方法,在最后几行:

    new-instance v0, Lcom/baidu/bus/d/i;
    iget-object v1, p0, Lcom/baidu/bus/activity/OfflineDataManageActivity;->U:Landroid/os/Handler;
    invoke-direct {v0, v1}, Lcom/baidu/bus/d/i;-><init>(Landroid/os/Handler;)V
    iput-object v0, p0, Lcom/baidu/bus/activity/OfflineDataManageActivity;->A:Lcom/baidu/bus/d/i;
    iget-object v0, p0, Lcom/baidu/bus/activity/OfflineDataManageActivity;->A:Lcom/baidu/bus/d/i;
    invoke-virtual {v0}, Lcom/baidu/bus/d/i;->a()V

    翻译为Java代码就是:

    this.A = new com.baidu.bus.d.i(this.U);
    this.A.a();

    上面的 U 是一个 com.baidu.bus.activity.bv 的对象,bv 是从 Handler 继承的类。首先看 A.a() 干了什么。

    2. 在com.baidu.bus.d.i 类中找到 a() 方法,关键的代码是下面几行:

    new-instance v0, Lcom/baidu/bus/net/b/a;
    const-string v1, "city_list"
    invoke-direct {v0, v1}, Lcom/baidu/bus/net/b/a;-><init>(Ljava/lang/String;)V
    iput-object v0, p0, Lcom/baidu/bus/d/i;->c:Lcom/baidu/bus/net/b/a;
    iget-object v0, p0, Lcom/baidu/bus/d/i;->c:Lcom/baidu/bus/net/b/a;
    iget-object v1, p0, Lcom/baidu/bus/d/i;->e:Lcom/baidu/bus/net/a/c;
    invoke-virtual {v0, v1}, Lcom/baidu/bus/net/b/a;->a(Lcom/baidu/a/a/k;)Z
    sget-object v0, Lcom/baidu/bus/activity/App;->g:Lcom/baidu/a/a/l;
    invoke-virtual {v0}, Lcom/baidu/a/a/l;->a()Lcom/baidu/a/a/h;
    move-result-object v0
    iget-object v1, p0, Lcom/baidu/bus/d/i;->c:Lcom/baidu/bus/net/b/a;
    invoke-virtual {v0, v1}, Lcom/baidu/a/a/h;->c(Lcom/baidu/a/a/m;)Z

    这几行的代码翻译为Java代码就是:

    this.c = new com.baidu.bus.net.b.a("city_list");
    this.c.a(this.e);
    App.g.a().c(this.c);

    在这里,e 成员是com.baidu.bus.net.a.c 的对象,查看这个类:

    .class public abstract Lcom/baidu/bus/net/a/c;
    .super Ljava/lang/Object;
    .implements Lcom/baidu/a/a/k;
    .implements Lcom/baidu/bus/net/a/a;

    实现了两个接口类,分别是 com.baidu.a.a.k 和 com.baidu.bus.net.a.a,从构造函数中:

    const-string v0, "<<ModelCallBack"
    iput-object v0, p0, Lcom/baidu/bus/net/a/c;->b:Ljava/lang/String;

    这两行代码是将类中的成员b赋了一个“<<ModelCallBack”的字符串,以此可以推测,这个类是HTTP收到 Response 后的回调。

    3. 继续看 com.baidu.bus.net.b.a 类:

    .class public final Lcom/baidu/bus/net/b/a;
    .super Lcom/baidu/bus/base/f;

    从 com.baidu.bus.base.f 类继承而来,没有 a() 方法,应该是父类的方法。但在这个类的构造函数里面可以看到:

    const-string v0, "http://bs.baidu.com/offlinebusdata/prov_city_list.json"
    iput-object v0, p0, Lcom/baidu/bus/net/b/a;->c:Ljava/lang/String;
    iput-object p1, p0, Lcom/baidu/bus/net/b/a;->b:Ljava/lang/String;

    翻译为Java代码是:

    this.c = "http://bs.baidu.com/offlinebusdata/prov_city_list.json";
    this.b = paramString;

    这里的 paramString 就是传入的 city_list 字符串。在浏览器中查看this.c 的地址,下载了一个 prov_city_list.json 的文件,打开可以看到下面内容:

    {
        "errorNo": 0, 
        "name": "中国", 
        "allList": [...],
        "hotCityList": [...]
    }

    在这里将城市列表隐去,在里面可以查看城市的 id 和 name。

    4. 继续看com.baidu.bus.base.f 类:

    .class public abstract Lcom/baidu/bus/base/f;
    .super Lcom/baidu/a/a/m;

    可以看到,父类是 com.baidu.a.a.m,在这个类里面有 a() 方法:

    .method public final a()Ljava/lang/Object;
    .method public final a(Ljava/lang/Object;)V
    .method public final a(Ljava/lang/String;Ljava/lang/String;)V
    .method public final declared-synchronized a(Lcom/baidu/a/a/k;)Z

    有多个 a() 方法。这就是代码混淆后的迷惑作用。由于在 2 中传入的参数实现了 k 接口,因此要看最后一个方法,这个方法里调用了父类的 b() 方法:

    invoke-super {p0, p1}, Lcom/baidu/a/a/m;->b(Lcom/baidu/a/a/k;)Z

    5. 查看 com.baidu.a.a.m 类的 b() 方法,这个方法的关键代码就是下面这行:

    iget-object v0, p0, Lcom/baidu/a/a/m;->g:Ljava/util/List;
    invoke-interface {v0, p1}, Ljava/util/List;->add(Ljava/lang/Object;)Z

    把参数传过来的 com.baidu.a.a.k 对象添加到 g 成员里,g 就是一个 List. 因此 2 中的:

    this.c.a(this.e);

    就是向类成员 c (com.baidu.bus.net.b.a) 包含的一个成员中的 List 中添加了一个用于回调的 e (com.baidu.bus.net.a.c) 对象。接下来是下一行:

    App.g.a().c(this.c);

    无脑猜测,这行代码就是要执行c对象,也就是下载 http://bs.baidu.com/offlinebusdata/prov_city_list.json 这个链接了。

    6. App.g 是一个 com.baidu.a.a.l 的对象,它的 a() 方法返回一个 com.baidu.a.a.h 的对象,查看 h 里面的 c() 方法。在这个方法中,可以看到下面代码:

    new-instance v3, Lcom/baidu/a/a/c;
    iget-object v4, p0, Lcom/baidu/a/a/h;->a:Lcom/baidu/a/a/l;
    invoke-direct {v3, v4, p1}, Lcom/baidu/a/a/c;-><init>(Lcom/baidu/a/a/l;Lcom/baidu/a/a/m;)V

    这是根据 this.a (com.baidu.a.a.h) 和传入的 com.baidu.a.a.m 对象构造了一个 com.baidu.a.a.c 的对象,然后调用了 c.c() 方法,返回 Boolean 类型:

    invoke-virtual {v3}, Lcom/baidu/a/a/c;->c()Z

    7. 查看 com.baidu.a.a.c 中的 c() 方法,这个方法包含下面代码:

    new-instance v0, Lcom/baidu/a/a/d;
    invoke-direct {v0, p0}, Lcom/baidu/a/a/d;-><init>(Lcom/baidu/a/a/c;)V
    iput-object v0, p0, Lcom/baidu/a/a/c;->d:Lcom/baidu/a/a/d;
    iget-object v0, p0, Lcom/baidu/a/a/c;->d:Lcom/baidu/a/a/d;
    invoke-virtual {v0}, Lcom/baidu/a/a/d;->start()V

    8. 查看 com.baidu.a.a.d 类:

    .class final Lcom/baidu/a/a/d;
    .super Ljava/lang/Thread;

    从 Thread 派生出的类,那就查看 run() 方法,有下面两行代码:

    iget-object v0, p0, Lcom/baidu/a/a/d;->a:Lcom/baidu/a/a/c;
    invoke-virtual {v0}, Lcom/baidu/a/a/c;->d()Lorg/apache/http/HttpResponse;

    在构造函数中可以看到,this.a 就是传入的 com.baidu.a.a.c 对象,在这里调用了它的 d() 方法,结果返回的是一个 HttpResponse 对象。

    9. 查看 com.baidu.a.a.c 里面的 d() 方法,现在可以断定这个方法是发送HTTP请求,因此直接查找相关的代码:

    new-instance v0, Lorg/apache/http/impl/client/DefaultHttpClient;
    invoke-direct {v0, v1}, Lorg/apache/http/impl/client/DefaultHttpClient;-><init>(Lorg/apache/http/params/HttpParams;)V

    构造了一个 DefaultHttpClient 对象,传入的参数暂时忽略。查看调用 execute 的地方:

    iget-object v1, p0, Lcom/baidu/a/a/c;->a:Lcom/baidu/a/a/m;
    invoke-virtual {v1}, Lcom/baidu/a/a/m;->b()Lorg/apache/http/client/methods/HttpUriRequest;
    move-result-object v1
    invoke-virtual {v0, v1}, Lorg/apache/http/impl/client/AbstractHttpClient;->execute(Lorg/apache/http/client/methods/HttpUriRequest;)Lorg/apache/http/HttpResponse;

    在构造函数中可以看到,this.a 就是传入的第二个参数,一个 com.baidu.a.a.m 对象,回到第(6)中可以知道,这个对象是在(2)中构造的c对象:

    this.c = new com.baidu.bus.net.b.a("city_list");

    10. 查看 com.baidu.bus.net.b.a 的 b() 方法:

    .method public final b()Lorg/apache/http/client/methods/HttpUriRequest;
        .locals 3
        new-instance v0, Lorg/apache/http/client/methods/HttpGet;
        iget-object v1, p0, Lcom/baidu/bus/net/b/a;->c:Ljava/lang/String;
        invoke-direct {v0, v1}, Lorg/apache/http/client/methods/HttpGet;-><init>(Ljava/lang/String;)V
        const-string v1, "Accept-Encoding"
        const-string v2, "gzip,deflate"
        invoke-interface {v0, v1, v2}, Lorg/apache/http/client/methods/HttpUriRequest;->addHeader(Ljava/lang/String;Ljava/lang/String;)V
        return-object v0
    .end method

    根据 this.c 成员(字符串)构造了一个HttpGet对象,添加了一个Header (Accept-Encoding: gzip,deflate),然后返回这个 HttpGet 对象。从(3)里可以知道,this.c 就是这个URL:
    http://bs.baidu.com/offlinebusdata/prov_city_list.json

    11. 这且还没有完,还没有解决下载后的数据如何解析,如何在 OfflineDataManageActivity 中展示。

    12. 继续查看 com.baidu.a.a.d (从Thread类派生)类的 run() 方法,在取得 HttpResponse 之后,调用了两个静态方法:

    iget-object v2, p0, Lcom/baidu/a/a/d;->a:Lcom/baidu/a/a/c;
    iget-object v3, p0, Lcom/baidu/a/a/d;->a:Lcom/baidu/a/a/c;
    invoke-static {v3}, Lcom/baidu/a/a/c;->c(Lcom/baidu/a/a/c;)Lcom/baidu/a/a/m;
    move-result-object v3
    invoke-virtual {v3}, Lcom/baidu/a/a/m;->e()I
    invoke-static {v2, p0, v0}, Lcom/baidu/a/a/c;->a(Lcom/baidu/a/a/c;Lcom/baidu/a/a/d;Lorg/apache/http/HttpResponse;)V

    查看 com.baidu.bus.a.a.c 类中的 a() 方法,调用了 HttpResponse 的 getEntity() ,获取 HttpEntity 之后调用了 getContent 获取了一个 InputStream,并调用 read() 读取了里面的内容,但是没有对 bytes 做任何处理。忽略。   

    iget-object v1, p0, Lcom/baidu/a/a/d;->a:Lcom/baidu/a/a/c;
    invoke-static {v1}, Lcom/baidu/a/a/c;->a(Lcom/baidu/a/a/c;)Lcom/baidu/a/a/e;
    move-result-object v1
    invoke-virtual {v1, v0}, Lcom/baidu/a/a/e;->a(Lorg/apache/http/HttpResponse;)V

    调用 com.baidu.a.a.c 里的一个静态方法 a() 获取一个 com.baidu.a.a.e 对象,然后调用 e 对象的 a() 方法,传入 HttpResponse。

    13. 查看 com.baidu.a.a.e 中的 a() 方法,这个方法是分发 HttpResponse,对成员 b (com.baidu.a.a.m) 中的一个 List 中包含的所有对象分发 HttpResponse。主要代码是下面两行:

    iget-object v2, p0, Lcom/baidu/a/a/e;->b:Lcom/baidu/a/a/m;
    invoke-interface {v0, v2, p1}, Lcom/baidu/a/a/k;->a(Lcom/baidu/a/a/m;Lorg/apache/http/HttpResponse;)V

    上面的 v0 的类型是 com.baidu.a.a.k,是这样取得的:

    iget-object v0, p0, Lcom/baidu/a/a/e;->b:Lcom/baidu/a/a/m;
    invoke-virtual {v0}, Lcom/baidu/a/a/m;->c()Ljava/util/List;
    move-result-object v0
    invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;
    move-result-object v1
    ...
    invoke-interface {v1}, Ljava/util/Iterator;->next()Ljava/lang/Object;
    move-result-object v0
    check-cast v0, Lcom/baidu/a/a/k;

    List list = this.b.c();
    iter = list.iterator();
    v0 = iter.next();

    成员 b 是一个 com.baidu.a.a.m 的对象,它的 c() 方法返回对象内的 g 成员,这是一个 List 的对象(ArrayList)。在 (5) 中我们知道,com.baidu.bus.d.i 向这个 List 里添加了一个元素,I 类中的 e (com.baidu.bus.net.a.c). 现在就调用它的 a() 方法:

    v0.a(this.b, httpResponse);

    同时我们知道,com.baidu.bus.net.a.c 实现了接口类 com.baidu.a.a.k,所以调用 k.a() 是完全没有问题的。

    14. 接下来看 com.baidu.bus.net.a.c 类的 a() 方法,这个方法主要是读取 HttpResponse 中的数据,稍微特殊一点的地方就是,如果内容是压缩的(Content-Encoding: gzip),那么回调用com.baidu.bus.i.f 类的 a() 方法,看起来这个类是解压 gzip 的。获取到内容后,用UTF-8 进行解码,然后看下面代码:

    new-instance v1, Landroid/os/Message;
    invoke-direct {v1}, Landroid/os/Message;-><init>()V
    const/4 v2, 0x4
    iput v2, v1, Landroid/os/Message;->what:I
    ...
    iput-object v2, v1, Landroid/os/Message;->obj:Ljava/lang/Object;
    
    iget-object v0, p0, Lcom/baidu/bus/net/a/c;->a:Landroid/os/Handler;
    invoke-virtual {v0, v1}, Landroid/os/Handler;->sendMessage(Landroid/os/Message;)Z

    这里的 hObject 是一个 com.baidu.bus.net.a.h 的对象,h 类是一个简单的数据类,定义如下:

    public final class h {
      public com.baidu.a.a.m a;
      public HttpResponse b;
      public com.baidu.bus.f.a c;
    }

    在 com.baidu.bus.net.a.c 类的 a() 方法中,只为h类中的 a 和 c 进行了赋值:

    invoke-static {v0, p1}, Lcom/baidu/bus/net/a/b;->a(Ljava/lang/String;Lcom/baidu/a/a/m;)Lcom/baidu/bus/f/a;
    move-result-object v0
    ...
    iput-object v0, v2, Lcom/baidu/bus/net/a/h;->c:Lcom/baidu/bus/f/a;
    iput-object p1, v2, Lcom/baidu/bus/net/a/h;->a:Lcom/baidu/a/a/m;

    上面的 m 就是向 a() 方法传递的第1个参数,类型是 com.baidu.a.a.m,即 com.baidu.a.a.e 类中的 b 成员。处理这个消息的 a 是 com.baidu.bus.net.a.d 类的对象。

    15. 查看 com.baidu.bus.net.a.d 的 handleMessage() 方法,取出 Message 的 what 值,进入一个 switch 语句,上面传入的值是4,因此进入了 pswitch_3。

    iget-object v1, p0, Lcom/baidu/bus/net/a/d;->a:Lcom/baidu/bus/net/a/c;
    iget-object v2, v0, Lcom/baidu/bus/net/a/h;->c:Lcom/baidu/bus/f/a;
    …
    iget-object v0, v0, Lcom/baidu/bus/net/a/h;->a:Lcom/baidu/a/a/m;
    invoke-virtual {v1, v2, v0}, Lcom/baidu/bus/net/a/c;->a(Lcom/baidu/bus/f/a;Lcom/baidu/a/a/m;)V

    类里的 a 成员是一个 com.baidu.bus.net.a.c 的对象,它是在 com.baidu.bus.net.a.d 初始化时传入的参数;com.baidu.bus.net.a.d 初始化是在 com.baidu.bus.net.a.c 中的,传入的是 this 。因此接下来查看 com.baidu.bus.net.a.c 类的 a() 方法。

    16. 查看 com.baidu.bus.net.a.c 类的 a() 方法,很可惜,没有找到合适的方法。观察到 c 类是一个抽象类,并且从 (2) 中得知,它的实例是 com.baidu.bus.d.i 类中的成员 e,查看 com.baidu.bus.d.i 类中 e 的初始化:

    new-instance v0, Lcom/baidu/bus/d/j;
    invoke-direct {v0, p0}, Lcom/baidu/bus/d/j;-><init>(Lcom/baidu/bus/d/i;)V
    iput-object v0, p0, Lcom/baidu/bus/d/i;->e:Lcom/baidu/bus/net/a/c;

    因此要从 com.baidu.bus.d.j 类中找相应的 a() 方法,查看com.baidu.bus.d.j 类:

    .class final Lcom/baidu/bus/d/j;
    .super Lcom/baidu/bus/net/a/c;

    果然从 com.baidu.bus.net.a.c 继承,并且只有一个方法,正是我们要找的方法。

    17. 查看 com.baidu.bus.d.j 中的 a() 方法,看到下面的代码:

    iget-object v0, p0, Lcom/baidu/bus/d/j;->a:Lcom/baidu/bus/d/i;
    check-cast p1, Lcom/baidu/bus/f/b;
    invoke-static {v0, p1}, Lcom/baidu/bus/d/i;->a(Lcom/baidu/bus/d/i;Lcom/baidu/bus/f/b;)V

    可以看到,调用了 com.baidu.bus.d.i 的静态方法 a(),第一个参数 this.a 是一个 com.baidu.bus.d.i 的对象,第二个参数是传入的第一个参数,即上面的 hObject.a,并转换为 com.baidu.bus.f.b 类型。查看 com.baidu.bus.d.i 中的 a() 方法,只是将传入的 paramA 赋给了类里面的成员d。回到 com.baidu.bus.d.j 中继续看下面的代码。然后,发送了一个Message:

    new-instance v0, Landroid/os/Message;
    invoke-direct {v0}, Landroid/os/Message;-><init>()V
    const/16 v1, 0x3e9
    iput v1, v0, Landroid/os/Message;->what:I
    iget-object v1, p0, Lcom/baidu/bus/d/j;->a:Lcom/baidu/bus/d/i;
    invoke-static {v1}, Lcom/baidu/bus/d/i;->a(Lcom/baidu/bus/d/i;)Lcom/baidu/bus/f/b;
    move-result-object v1
    iput-object v1, v0, Landroid/os/Message;->obj:Ljava/lang/Object;
    iget-object v1, p0, Lcom/baidu/bus/d/j;->a:Lcom/baidu/bus/d/i;
    invoke-static {v1}, Lcom/baidu/bus/d/i;->b(Lcom/baidu/bus/d/i;)Landroid/os/Handler;
    move-result-object v1
    invoke-virtual {v1, v0}, Landroid/os/Handler;->sendMessage(Landroid/os/Message;)Z

    翻译为Java代码:

    message = new Message();
    message.what = 0x3e9; // 1001
    message.obj = com.baidu.bus.d.i.a(this.a);
    com.baidu.bus.d.i.b(this.a).sendMessage(message);

    创建 Message,what 赋值为1001;查看 com.baidu.bus.d.i 中的 a() 方法(需要注意参数的类型和个数,混淆后的代码,有很多函数名是一样的,只能根据参数类型和个数区分),只是获取类里面的成员d(还记得刚才赋过值吗),其实就是刚才传入的 paramA,即这个函数内的第一个参数;然后调用 b() 方法,取得了类里面的 b 成员,它是个 Handler 对象,发送了创建的 Message。通过第1步我们知道,com.baidu.bus.d.i 里面的b成员,是 OfflineDataManageActivity 类里面的U成员,它是一个 com.baidu.bus.activity.bv 对象。

    18. 查看 com.baidu.bus.activity.bv 类里面的 handleMessage() 方法,在 packed-switch 中正好有我们要找的 0x3e9 (1001),并且只有这一个分支:

    .packed-switch 0x3e9
        :pswitch_0
    .end packed-switch

    在分支的代码中,找到这样的代码:

    iget-object v0, p1, Landroid/os/Message;->obj:Ljava/lang/Object;
    check-cast v0, Lcom/baidu/bus/f/b;
    invoke-static {v1, v0}, Lcom/baidu/bus/activity/OfflineDataManageActivity;->a(Lcom/baidu/bus/activity/OfflineDataManageActivity;Lcom/baidu/bus/f/b;)V

    19. 回到 OfflineDataManageActivity 中查看 a() 方法,直接把传入的对象赋给了 n 成员。再回到 com.baidu.bus.activity.bv 的 handleMessage() 方法中,接下来的代码:

    invoke-static {v1}, Lcom/baidu/bus/activity/OfflineDataManageActivity;->a(Lcom/baidu/bus/activity/OfflineDataManageActivity;)Lcom/baidu/bus/f/b;
    move-result-object v1
    invoke-static {v0, v1}, Lcom/baidu/bus/activity/OfflineDataManageActivity;->b(Lcom/baidu/bus/activity/OfflineDataManageActivity;Lcom/baidu/bus/f/b;)V

    不用想,第一行的 a() 方法肯定是获取刚才赋过值的 n 成员,接下来看它的 b() 方法。

    20. 查看 OfflineDataManageActivity 的 b() 方法,完整的代码如下:

    new-instance v0, Lcom/baidu/bus/activity/ch;
    invoke-direct {v0, p0}, Lcom/baidu/bus/activity/ch;-><init>(Lcom/baidu/bus/activity/OfflineDataManageActivity;)V
    iput-object v0, p0, Lcom/baidu/bus/activity/OfflineDataManageActivity;->B:Lcom/baidu/bus/activity/ch;
    iget-object v0, p0, Lcom/baidu/bus/activity/OfflineDataManageActivity;->B:Lcom/baidu/bus/activity/ch;
    const/4 v1, 0x1
    new-array v1, v1, [Lcom/baidu/bus/f/b;
    const/4 v2, 0x0
    aput-object p1, v1, v2
    invoke-virtual {v0, v1}, Lcom/baidu/bus/activity/ch;->execute([Ljava/lang/Object;)Landroid/os/AsyncTask;
    return-void

    翻译为Java代码:

    v0 = new com.baidu.bus.activity.ch(offlineDataManageActivity);
    v0.B = offlineDataManageActivity;
    v1 = new com.baidu.bus.f.b [] {p1};
    v0.execute(v1);

    上面的p1就是传入的 com.baidu.bus.f.b 对象。

    21. 查看 com.baidu.bus.activity.ch 类,可以看到,它是从 AsyncTask 派生出来的:

    .class final Lcom/baidu/bus/activity/ch;
    .super Landroid/os/AsyncTask;

    那么需要查看doInBackground() 方法,发现它直接调用了它的 a() 方法,查看 a() 方法。

    22. 查看 com.baidu.bus.activity.ch 类里的 a() 方法,这个方法很长而且很无聊,大体就是将传入的 com.baidu.bus.f.b 对象中的 b 成员,附加到 ExpandableListAdapter 上,我们只要略微了解一下ExpandableListView 和 ExpandableLsitAdapter 的用法即可。此处,附加的过程略去,只要知道以下结果:
    (1) OfflineDataManageActivity.L 保存省份,每个元素是 com.baidu.bus.b.f 的对象;
    (2) com.baidu.bus.b.f 类里面有一个 List 类型的成员d,它保存着该省份下面的每个城市,每个元素都是 com.baidu.bus.b.a 的对象。
    前面已经介绍了ExpandableView 如何响应点击事件,后面的过程就不赘述了。

  • 相关阅读:
    关于java和jvm的思考
    java之try、catch、finally
    Microsoft SQLServer有四种系统数据库
    HDU 5087
    uva639 暴力、回溯
    uva127
    uva 131
    洛谷 P2580 于是他错误的点名开始了
    字典树(trie)
    HTML学习笔记
  • 原文地址:https://www.cnblogs.com/zhangbaoqiang/p/5141703.html
Copyright © 2020-2023  润新知