Android ASM字節(jié)碼插樁(上)

一、ASM簡(jiǎn)介
ASM是一個(gè)字節(jié)碼操作框架,可用來(lái)動(dòng)態(tài)生成字節(jié)碼或者對(duì)現(xiàn)有的類進(jìn)行增強(qiáng)怜浅。ASM可以直接生成二進(jìn)制的class字節(jié)碼,也可以在class被加載進(jìn)虛擬機(jī)前動(dòng)態(tài)改變其行為童芹,比如方法執(zhí)行前后插入代碼、添加成員變量鲤拿、修改父類假褪、添加接口等等。

插樁就是將一段代碼插入或者替換原本的代碼皆愉。字節(jié)碼插樁就是在編寫的java源碼編譯成class字節(jié)碼后嗜价,在Android下生成dex之前修改class文件艇抠,修改或者增強(qiáng)原有代碼邏輯的操作幕庐。

二、引入ASM庫(kù)
可以訪問ASM官網(wǎng)家淤,https://asm.ow2.io/index.html异剥,更新asm的版本,目前最新版本9.3

在app/build.gradle下引入asm庫(kù)

dependencies {
    /**
     * 使用 testImplementation引入絮重,這表示我們只能在Java的單元測(cè)試中使用這個(gè)框架冤寿,
     * 對(duì)我們Android中的依賴關(guān)系沒有任何影響
     */
    testImplementation 'org.ow2.asm:asm:9.3'
    testImplementation 'org.ow2.asm:asm-commons:9.3'
}

引入asm庫(kù)之后,就在src\test目錄下編寫測(cè)試代碼


cfab9ccb35f2360f5bebe6c71a9ba0f.png

三青伤、使用ASM進(jìn)行字節(jié)碼插樁
應(yīng)用場(chǎng)景:通過字節(jié)碼插樁計(jì)算方法的執(zhí)行時(shí)間督怜。

3.1 首先編寫測(cè)試類InjectTest.java

package com.xyaty.asmdemo;

/**
 * DESC   : 測(cè)試類
 */
public class InjectTest {

    public static void main(String[] args) throws InterruptedException {
        //模擬方法執(zhí)行的時(shí)間
        Thread.sleep(1000);
    }

    public void methodA() {
        System.out.println("methodA");
    }
}

3.2 接下來(lái)對(duì)InjectTest.java文件通過javac命令進(jìn)行編譯成InjectTest.class
由于我們操作的是字節(jié)碼插樁,也就是class文件狠角,所以需要進(jìn)入 test/java下面使用 javac對(duì)這個(gè)java類進(jìn)行編譯生成對(duì)應(yīng)的class文件号杠,具體操作是:在Android studio底部Terminal窗口,通過cd進(jìn)入到test/java目錄下丰歌,然后執(zhí)行以下命令:

D:\work\plugin\ASMDemo\app\src\test\java>javac com\xyaty\asmdemo\InjectTest.java

執(zhí)行上面的命令編譯后姨蟋,就會(huì)在test/java下面生成對(duì)應(yīng)的InjectTest.class文件,這個(gè)class文件就是待插樁的文件立帖。

生成的InjectTest.class文件如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.xyaty.asmdemo;

public class InjectTest {
    public InjectTest() {
    }

    public static void main(String[] var0) throws InterruptedException {
        Thread.sleep(1000L);
    }

    public void methodA() {
        System.out.println("methodA");
    }
}

3.3 期望實(shí)現(xiàn)的效果就是利用ASM完成對(duì)InjectTest.class字節(jié)碼的插樁

public static void main(String[] args) throws InterruptedException {
        //方法開始的時(shí)間
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        //方法結(jié)束的時(shí)間
        long end = System.currentTimeMillis();
        //輸出方法執(zhí)行花費(fèi)的時(shí)間
        System.out.println("execute: "+(end - start)+"ms");
    }

3.4 編寫測(cè)試類InjectUnitTest.java執(zhí)行插樁

package com.xyaty.asmdemo;

import org.junit.Test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * DESC   :
 */
public class InjectUnitTest {
    /**
     * 單元測(cè)試方法眼溶,右擊test()方法,選擇run test()方法即可查看結(jié)果
     */
    @Test
    public void test() {
        try {
            //讀取待插樁的class
            FileInputStream fis = new FileInputStream(
                    new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));

            /**
             * 執(zhí)行分析與插樁
             * ClassReader是class字節(jié)碼的讀取與分析引擎
             */
            ClassReader classReader = new ClassReader(fis);
            // ClassWriter寫出器晓勇, COMPUTE_FRAMES表示自動(dòng)計(jì)算棧幀和局部變量表的大小
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            /**
             * 執(zhí)行分析堂飞,處理結(jié)果寫入classWriter灌旧, EXPAND_FRAMES表示棧圖以擴(kuò)展格式進(jìn)行訪問
             * 執(zhí)行插樁的代碼就在MyClassVisitor中實(shí)現(xiàn)
             */
            classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);

            //獲得執(zhí)行了插樁之后的字節(jié)碼數(shù)據(jù)
            byte[] bytes = classWriter.toByteArray();
            // 重新寫入InjectTest.class中(也可以寫入到其他class中,InjectTest1.class)绰筛,完成插樁
            FileOutputStream fos = new FileOutputStream(
                    new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));
            fos.write(bytes);
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class MyClassVisitor extends ClassVisitor {

        public MyClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            System.out.println("visitMethod==>name="+name);
            /**
             * 會(huì)輸出以下方法:
             * visitMethod==>name=<init>
             * visitMethod==>name=main
             */
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name,descriptor);
        }
    }


    /**
     * 之所以繼承自AdviceAdapter节榜,是因?yàn)锳dviceAdapter是MethodVisitor的子類,
     * AdviceAdapter封裝了指令插入方法别智,更為直觀與簡(jiǎn)單宗苍,
     * 要使用其中的onMethodEnter和 onMethodExit方法進(jìn)行字節(jié)碼插樁,
     *
     * 繼承關(guān)系如下:
     * AdviceAdapter extends GeneratorAdapter
     * GeneratorAdapter extends LocalVariablesSorter
     * LocalVariablesSorter extends MethodVisitor
     */
    public class MyMethodVisitor extends AdviceAdapter {
        long start;
        private int startIdentifier;

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            System.out.println("visitAnnotation===>methodName="+getName()+", descriptor="+descriptor);
            return super.visitAnnotation(descriptor, visible);
        }

        protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
        }

        /**
         * 進(jìn)入方法插入內(nèi)容
         */
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();

//            start = System.currentTimeMillis();
            /**
             * @Type owner 調(diào)用哪個(gè)類
             * @Method method 調(diào)用某個(gè)類的靜態(tài)方法(參數(shù)name: 方法名字薄榛,descriptor:方法中參數(shù)和方法返回值類型)
             */
            invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
            //調(diào)用newLocal創(chuàng)建一個(gè)long類型的變量讳窟,返回一個(gè)int類型索引identifier
            startIdentifier = newLocal(Type.LONG_TYPE);
            //保存到本地變量索引中,用一個(gè)本地變量接收上一步執(zhí)行的結(jié)果
            storeLocal(startIdentifier);
        }

        /**
         * 在方法結(jié)尾插入內(nèi)容
         * @param opcode
         */
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);

//            long end = System.currentTimeMillis();
//            System.out.println("execute: "+(end - start)+"ms");

            invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
            //調(diào)用newLocal創(chuàng)建一個(gè)long類型的變量敞恋,返回一個(gè)int類型索引identifier
            int endIdentifier = newLocal(Type.LONG_TYPE);
            //保存到本地變量索引中丽啡,用一個(gè)本地變量接收上一步執(zhí)行的結(jié)果
            storeLocal(endIdentifier);

            //獲取System的靜態(tài)字段out,類型為PrintStream
            getStatic(Type.getType("Ljava/lang/System;"),
                    "out", Type.getType("Ljava/io/PrintStream;"));

            /**
             * "execute: "+(end - start)+"ms"實(shí)際是內(nèi)部創(chuàng)建StringBuilder來(lái)拼接
             * 源碼:NEW java/lang/StringBuilder
             * 創(chuàng)建一個(gè)對(duì)象StringBuilder
             */
            newInstance(Type.getType("Ljava/lang/StringBuilder;"));
            // dup壓入棧頂,讓下面的INVOKESPECIAL 知道執(zhí)行誰(shuí)的構(gòu)造方法創(chuàng)建StringBuilder
            dup();
            /**
             * 源碼:INVOKESPECIAL java/lang/StringBuilder.<init> ()V
             * 創(chuàng)建StringBuilder的構(gòu)造方法硬猫,用init來(lái)代替
             */
            invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("<init>", "()V"));

            visitLdcInsn("execute: ");
            /**
             * 源碼:INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
             * 調(diào)用append方法
             */
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
            /**
             * 對(duì)結(jié)束時(shí)間和開始時(shí)間進(jìn)行減法操作
             * LLOAD 3 先加載結(jié)束時(shí)間
             * LLOAD 1 后加載開始時(shí)間
             * LSUB    執(zhí)行減法操作
             */
            loadLocal(endIdentifier);
            loadLocal(startIdentifier);
            //執(zhí)行減法操作补箍,返回long類型
            math(SUB, Type.LONG_TYPE);

            /**
             * 源碼:INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
             * LDC "ms"
             */
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(J)Ljava/lang/StringBuilder;"));
            //拼接毫秒
            visitLdcInsn("ms");

            /**
             * 源碼:
             * INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
             * INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
             * INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
             */
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("toString", "()Ljava/lang/String;"));
            invokeVirtual(Type.getType("Ljava/io/PrintStream;"),
                    new Method("println", "(Ljava/lang/String;)V"));
        }

    }
}

在上述代碼中可以看到,
其實(shí)就是在onMethodEnter()方法中插入了:

long start = System.currentTimeMillis();

在onMethodExit()方法中插入了:

long end = System.currentTimeMillis();
System.out.println("execute: "+(end - start)+"ms");

但是使用字節(jié)碼指令卻寫了一大堆代碼啸蜜。

3.5 可以通過android studio的插件ASM(以下三選一)坑雅,查看指令代碼
(1)ASM Bytecode Viewer
(2)ASM Bytecode Viewer Support Kotlin
(3)ASM Bytecode Outline
在AS4.1以上版本使用ASM Bytecode Viewer和ASM Bytecode Outline都報(bào)錯(cuò),可能是AS升級(jí)后沒有做兼容衬横,使用ASM Bytecode Viewer Support Kotlin即可搞定(親測(cè))裹粤。

a275c0b4bdc548b8f9fec8e1dc19ad5.png

安裝好ASM之后,重啟AS蜂林,在InjectTest.java中右鍵點(diǎn)擊選項(xiàng)“ASM Bytecode Viewer”查看指令代碼

InjectTest.java文件

package com.xyaty.asmdemo;


/**
 * Author :
 * Date   : 2022/7/21
 * DESC   :
 */
public class InjectTest {

    @ASMTest
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println("execute: "+(end - start)+"ms");
    }

    public void methodA() {
        System.out.println("methodA");
    }
}

ASM指令代碼截圖:


778c04f87130c625967e140f11bf6dc.png

ASM指令代碼如下:

// class version 51.0 (51)
// access flags 0x21
public class com/xyaty/asmdemo/InjectTest {

  // compiled from: InjectTest.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V throws java/lang/InterruptedException 
  @Lcom/xyaty/asmdemo/ASMTest;() // invisible
   L0
    LINENUMBER 13 L0
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 1
   L1
    LINENUMBER 14 L1
    LDC 1000
    INVOKESTATIC java/lang/Thread.sleep (J)V
   L2
    LINENUMBER 15 L2
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 3
   L3
    LINENUMBER 16 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "execute: "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LLOAD 3
    LLOAD 1
    LSUB
    INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
    LDC "ms"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 17 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE start J L1 L5 1
    LOCALVARIABLE end J L3 L5 3
    MAXSTACK = 6
    MAXLOCALS = 5

  // access flags 0x1
  public methodA()V
   L0
    LINENUMBER 20 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "methodA"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 21 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

這些指令涉及到了java類型描述符和方法描述遥诉,


f0d7a05a07f4257db0a53660bebb9bd.png
59a8e46f68d9904b087b3e0c49fcafc.png

①類型描述符
Java代碼中的類型,在字節(jié)碼中有相應(yīng)的表示協(xié)議:
(1)Java基本類型的描述符是單個(gè)字符噪叙,例如Z表示boolean矮锈、C表示char
(2)類的類型的描述符是這個(gè)類的全限定名,前面加上字符L 睁蕾, 后面跟上一個(gè)「;」苞笨,例如String的類型描述符為L(zhǎng)java/lang/String;
(3)數(shù)組類型的描述符是一個(gè)方括號(hào)后面跟有該數(shù)組元素類型的描述符,多維數(shù)組則使用多個(gè)方括號(hào)惫霸。

借助上面的協(xié)議分析猫缭,想要看到字節(jié)碼中參數(shù)的類型,就比較簡(jiǎn)單了壹店。

②方法描述符
方法描述符(方法簽名)是一個(gè)類型描述符列表猜丹,它用一個(gè)字符串描述一個(gè)方法的參數(shù)類型和返回類型。

方法描述符以左括號(hào)開頭硅卢,然后是每個(gè)形參的類型描述符射窒,然后是是右括號(hào)藏杖,接下來(lái)是返回類型的類型描述符,例如脉顿,該方法返回void蝌麸,則是V,要注意的是艾疟,方法描述符中不包含方法的名字或參數(shù)名来吩。

比如:
void m(int i, float f)對(duì)應(yīng)的方法描述符是(IF)V ,表明該方法會(huì)接收一個(gè)int和float型參數(shù)蔽莱,且無(wú)返回值弟疆。
int m(Object o)對(duì)應(yīng)的方法描述符是(Ljava/lang/Object;)I 表示接收Object型參數(shù),返回int盗冷。
int[] m(int i, String s)對(duì)應(yīng)的方法描述符是(ILjava/lang/String;)[I 表示接受int和String怠苔,返回一個(gè)int[]。
Object m(int[] i)對(duì)應(yīng)的方法描述符是 ([I)Ljava/lang/Object; 表示接受一個(gè)int[]仪糖,返回Object柑司。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市锅劝,隨后出現(xiàn)的幾起案子攒驰,更是在濱河造成了極大的恐慌,老刑警劉巖鸠天,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讼育,死亡現(xiàn)場(chǎng)離奇詭異帐姻,居然都是意外死亡稠集,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門饥瓷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)剥纷,“玉大人,你說我怎么就攤上這事呢铆』扌” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵棺克,是天一觀的道長(zhǎng)悠垛。 經(jīng)常有香客問我,道長(zhǎng)娜谊,這世上最難降的妖魔是什么确买? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮纱皆,結(jié)果婚禮上湾趾,老公的妹妹穿的比我還像新娘芭商。我一直安慰自己,他們只是感情好搀缠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布铛楣。 她就那樣靜靜地躺著,像睡著了一般艺普。 火紅的嫁衣襯著肌膚如雪簸州。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天歧譬,我揣著相機(jī)與錄音勿侯,去河邊找鬼。 笑死缴罗,一個(gè)胖子當(dāng)著我的面吹牛助琐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播面氓,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼兵钮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了舌界?” 一聲冷哼從身側(cè)響起掘譬,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呻拌,沒想到半個(gè)月后葱轩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡藐握,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年靴拱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猾普。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡袜炕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出初家,到底是詐尸還是另有隱情偎窘,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布溜在,位于F島的核電站陌知,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏掖肋。R本人自食惡果不足惜仆葡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望培遵。 院中可真熱鬧浙芙,春花似錦登刺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至南窗,卻和暖如春揍很,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背万伤。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工窒悔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人敌买。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓简珠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親虹钮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子聋庵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容