0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

動(dòng)態(tài)函數(shù)接口的調(diào)用原理

科技綠洲 ? 來(lái)源:Java技術(shù)指北 ? 作者:Java技術(shù)指北 ? 2023-10-13 11:27 ? 次閱讀

本篇將從編譯,執(zhí)行層面為大家講解函數(shù)式接口運(yùn)行的機(jī)制,讓各位小伙伴更進(jìn)一步加深對(duì)函數(shù)式接口的理解

概述

函數(shù)式接口包含三部分內(nèi)容:

  • (應(yīng)用篇一JDK源碼解析——深入函數(shù)式接口(應(yīng)用篇一))(1)函數(shù)式接口的來(lái)源,(2)Lambda表達(dá)式,(3)雙冒號(hào)運(yùn)算符
  • (應(yīng)用篇二函數(shù)式編程,這樣學(xué)就廢了)(4)詳細(xì)介紹@FunctionInterface注解(5)對(duì)java.util.function包進(jìn)行解讀
  • (原理篇)介紹函數(shù)式接口的實(shí)現(xiàn)原理 在看本篇之前,請(qǐng)大家對(duì)應(yīng)先看應(yīng)用篇一和應(yīng)用篇二,本篇作為原理篇,將為大家較為深入的剖析函數(shù)式接口如何編譯,JVM又是如何關(guān)聯(lián)銜接各個(gè)部分的。

說(shuō)明:源碼使用的版本為JDK-11.0.11

編譯

首先我們從編譯出發(fā),因?yàn)闊o(wú)論是接口還是類,都需要經(jīng)過(guò)編譯,然后在運(yùn)行期由JVM執(zhí)行調(diào)用,現(xiàn)在我們來(lái)看看幾個(gè)關(guān)鍵位置的編譯結(jié)果。先來(lái)看函數(shù)式接口編譯

Classfile /O:/SCM/ws-java/sample-lambda/bin/com/tree/sample/func/IFuncInterfaceSample.class
  Last modified 2021-6-4; size 238 bytes
  MD5 checksum 58a3c8c5cbe9c7498e86d4a349554ae0
  Compiled from "IFuncInterfaceSample.java"
public interface com.tree.sample.func.IFuncInterfaceSample
  minor version: 0
  major version: 55
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
   #1 = Class              #2             // com/tree/sample/func/IFuncInterfaceSample
   #2 = Utf8               com/tree/sample/func/IFuncInterfaceSample
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               func1
   #6 = Utf8               ()V
   #7 = Utf8               SourceFile
   #8 = Utf8               IFuncInterfaceSample.java
   #9 = Utf8               RuntimeVisibleAnnotations
  #10 = Utf8               Ljava/lang/FunctionalInterface;
{
  public abstract void func1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "IFuncInterfaceSample.java"
RuntimeVisibleAnnotations:
  0: #10()

接口的編譯信息中沒(méi)有任何額外的工作,如果顯示聲明了FunctionInterface注解,則編譯信息中帶有,反之則無(wú)。

接下來(lái),我們著重來(lái)看應(yīng)用部分的代碼編譯的情況,先看應(yīng)用部分的源代碼:

public class LambdaBinaryCode {
    private int lambdaVar = 100;
    
    public static void main(String[] args) {
        
        LambdaBinaryCode ins = new LambdaBinaryCode();
        ins.invokeLambda();
        ins.invokeEta();
        ins.invokeLambda2();
    }
    
    /**
     * 簡(jiǎn)單的函數(shù)式編程示例
     */
    public void invokeLambda() {
        // 準(zhǔn)備測(cè)試數(shù)據(jù)
        Integer[] data = new Integer[] {1, 2, 3};
        List< Integer > list = Arrays.asList(data);
        
        // 簡(jiǎn)單示例:打印List數(shù)據(jù)
        list.forEach(x - > System.out.println(String.format("Cents into Yuan: %.2f", x/100.0)));
    }
    
    /**
     * 簡(jiǎn)單的函數(shù)式編程示例
     */
    public void invokeEta() {
        // 準(zhǔn)備測(cè)試數(shù)據(jù)
        Integer[] data = new Integer[] {1, 2, 3};
        List< Integer > list = Arrays.asList(data);
        
        // 通過(guò)eta操作符訪問(wèn)
        list.forEach(System.out::println);
    }
    
    /**
     * 簡(jiǎn)單的函數(shù)式編程示例
     */
    public void invokeLambda2() {
        // 準(zhǔn)備測(cè)試數(shù)據(jù)
        Map< Integer, Integer > map = new HashMap< Integer, Integer >();
        int count = 10;
        Random r = new Random();
        while(count-- >0) {
            map.put(r.nextInt(100), r.nextInt(10000));
        }
        
        // Lambda調(diào)用示例
        map.forEach((x, y) - > {
            System.out.println(String.format("Map key: %1s, value: %2s", x, y+lambdaVar));
        });
    }
}

這段源碼中選取了幾種典型的場(chǎng)景進(jìn)行組合,讓大家了解更多的擴(kuò)展知識(shí),因此代碼稍顯長(zhǎng)。

  • invokeLambda() 單個(gè)參數(shù)的lambda表達(dá)式,省略參數(shù)括號(hào)和表達(dá)式主體的花括號(hào)。
  • invokeEta() eta方式的方法引用。
  • invokeLambda2() 兩個(gè)參數(shù)的lambda表達(dá)式,lambda中使用成員變量。

lambda表達(dá)式的編譯

指北君和大家一起看看編譯后的內(nèi)容,使用命令查看編譯后的方法結(jié)構(gòu)(javap -p com.tree.sample.func.LambdaBinaryCode)

Compiled from "LambdaBinaryCode.java"
public class com.tree.sample.func.LambdaBinaryCode {
  private int lambdaVar;
  public com.tree.sample.func.LambdaBinaryCode();
  public static void main(java.lang.String[]);
  public void invokeLambda();
  public void invokeEta();
  public void invokeLambda2();
  private static void lambda$0(java.lang.Integer);
  private void lambda$2(java.lang.Integer, java.lang.Integer);
}

小伙伴有沒(méi)發(fā)現(xiàn),class文件中比源碼文件中多出了兩個(gè)方法:lambda2。這兩個(gè)方法分別對(duì)應(yīng)invokeLambda和invokeLambda2中的的lambda表達(dá)式。

我們?cè)趈avap命令中增加-v參數(shù),可以查看到增加的 方法的更多細(xì)節(jié),不熟悉JVM指令的小伙伴也不用擔(dān)心,我們只是驗(yàn)證 就是invokeLambda中l(wèi)ambda表達(dá)式對(duì)應(yīng)“x -> System.out.println(String.format("Cents into Yuan: %.2f", x/100.0))”。

private static void lambda$0(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=9, locals=1, args_size=1
         0: getstatic     #61                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #105                // String Cents into Yuan: %.2f
         5: iconst_1
         6: anewarray     #3                  // class java/lang/Object
         9: dup
        10: iconst_0
        11: aload_0
        12: invokevirtual #107                // Method java/lang/Integer.intValue:()I
        15: i2d
        16: ldc2_w        #111                // double 100.0d
        19: ddiv
        20: invokestatic  #113                // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        23: aastore
        24: invokestatic  #118                // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
        27: invokevirtual #124                // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        30: return
      LineNumberTable:
        line 30: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0     x   Ljava/lang/Integer;

從編譯信息中我們可以看到幾條明顯相同的邏輯:

  • LocalVariableTable 首先包含了函數(shù)的輸入?yún)?shù),并且一致
  • 24行執(zhí)行String.format方法
  • 27行執(zhí)行PrintStream.println方法 從上面三個(gè)關(guān)鍵部分我們可以確定就是invokeLambda方法中的lambda表達(dá)式編譯后的內(nèi)容了。

仔細(xì)的小伙伴比較 和 兩個(gè)方法后,可能會(huì)發(fā)現(xiàn)兩個(gè)問(wèn)題:

  1. 兩個(gè)方法怎么一個(gè)是static一個(gè)是非static的呢?
  2. 方法命名中的數(shù)字為什么不是數(shù)字連續(xù)的?
    對(duì)于第一個(gè)問(wèn)題,比較invokeLambda和invokeLambda2的源碼,小伙伴發(fā)現(xiàn)有什么不同么?是否可以看到invokeLambda2中的lambda表達(dá)式引用了成員屬性lambdaVar。這就是lambda生成方法的一種邏輯, 未使用成員變量的lambda表達(dá)式編譯成靜態(tài)方法,使用了成員變量的lambda語(yǔ)句則編譯為成員方法 。

第二個(gè)問(wèn)題我們將留待后面回答。

Lambda調(diào)用

上面我們看到了lambda表達(dá)式的代碼編譯成了一個(gè)獨(dú)立方法,指北君繼續(xù)帶領(lǐng)大家查看編譯后的文件,我們要了解編譯后lambda方法是如何調(diào)用執(zhí)行的。查看invokeLambda方法的編譯后的內(nèi)容(直貼出了關(guān)鍵部分):

public void invokeLambda();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=1
        ... ...
        32: istore_3
        33: aload_2
        34: invokedynamic #45,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
        39: invokeinterface #49,  2           // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)
        ... ...

在invokeLambda中有一個(gè)指令invokedynamic,熟悉動(dòng)態(tài)語(yǔ)言的小伙伴可能知道,這個(gè)指令是Java7為支持動(dòng)態(tài)腳本語(yǔ)言而增加的。而函數(shù)式Java調(diào)用函數(shù)接口也正是通過(guò)invokedynamic指令來(lái)實(shí)現(xiàn)的。invokeLambda的詳細(xì)內(nèi)容指北君后續(xù)單獨(dú)為大家講解,今天我們關(guān)注函數(shù)接口的調(diào)用過(guò)程。

使用invokeLambda指令,那么該指令是直接調(diào)用的lambda$0方法么?我們知道list.forEach(xx)調(diào)用中,我們是將函數(shù)接口作為參數(shù)傳遞到其他類的函數(shù)中進(jìn)行執(zhí)行的。Java需要解決兩個(gè)問(wèn)題:
1)如何將方法傳遞給被調(diào)用的外部類的方法。
2)外部的類和方法如何訪問(wèn)我們內(nèi)部私有的方法。

引導(dǎo)方法表

為解決上面兩個(gè)問(wèn)題,我們繼續(xù)查編譯后的文件,在末尾,我們看到下面的部分:

BootstrapMethods:
  0: #146 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #148 (Ljava/lang/Object;)V
      #151 invokestatic com/tree/sample/func/LambdaBinaryCode.lambda$0:(Ljava/lang/Integer;)V
      #152 (Ljava/lang/Integer;)V
  1: #146 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #153 (Ljava/lang/Object;)V
      #156 invokevirtual java/io/PrintStream.println:(Ljava/lang/Object;)V
      #157 (Ljava/lang/Integer;)V
  2: #146 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #159 (Ljava/lang/Object;Ljava/lang/Object;)V
      #162 invokespecial com/tree/sample/func/LambdaBinaryCode.lambda$2:(Ljava/lang/Integer;Ljava/lang/Integer;)V
      #163 (Ljava/lang/Integer;Ljava/lang/Integer;)V
InnerClasses:
     public static final #169= #165 of #167; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

這生成了三個(gè)引導(dǎo)方法,剛好和我們的三個(gè)函數(shù)接口調(diào)用一致,從引導(dǎo)方法的參數(shù)我們看出

序號(hào)調(diào)用調(diào)用類型
0lambda$0 static
1PrintStream.printlnvertual
2lambda$2 special

順便回答一下之前的方法名稱的數(shù)字序號(hào)不連續(xù)問(wèn)題,我們看出,方法名稱的序號(hào)是根據(jù)引導(dǎo)方法的序號(hào)來(lái)確定的,不是根據(jù)生成的lambda表達(dá)式方法序號(hào)來(lái)的。我們看到,引導(dǎo)方法的邏輯似乎就是調(diào)用lambda方法或者其他的函數(shù)接口,每個(gè)引導(dǎo)方法中都出現(xiàn)了LambdaMetafactory.metafactory方法

動(dòng)態(tài)調(diào)用

現(xiàn)在,我們結(jié)合invokedynamic指令來(lái)說(shuō)明BootstrapMethods執(zhí)行的過(guò)程

圖片
動(dòng)態(tài)調(diào)用邏輯

上面的的流程顯示了動(dòng)態(tài)調(diào)用的基本邏輯

  1. 執(zhí)行invokedynamic
  2. 檢查調(diào)用點(diǎn)是否已連接可用

  1. 如果未連接,構(gòu)建動(dòng)態(tài)調(diào)用點(diǎn)
  2. 執(zhí)行引導(dǎo)方法
  3. 生成并加載調(diào)用點(diǎn)對(duì)應(yīng)的動(dòng)態(tài)內(nèi)部類
  4. 連接

  1. 調(diào)用動(dòng)態(tài)內(nèi)部類方法
  2. 內(nèi)部類調(diào)用lambda對(duì)應(yīng)的方法并執(zhí)行

這兩個(gè)階段我們通過(guò)調(diào)用堆棧也能明顯觀察到:

圖片引導(dǎo)階段

圖片
執(zhí)行階段

我們還可以通過(guò)設(shè)置VM參數(shù)-Djdk.internal.lambda.dumpProxyClasses,查看以引導(dǎo)階段動(dòng)態(tài)生成的內(nèi)部類:

圖片
動(dòng)態(tài)內(nèi)部類列表

打開(kāi)其中一個(gè)如下:

圖片
動(dòng)態(tài)內(nèi)部類詳情

小結(jié)

動(dòng)態(tài)函數(shù)接口的調(diào)用原理,給大家介紹到這里了,相信大家看完本篇內(nèi)容后,對(duì)函數(shù)式接口有了更深一層的學(xué)習(xí)。由于涉及的內(nèi)容較多,沒(méi)有時(shí)間給大家逐一詳細(xì)的給每個(gè)涉及到的類進(jìn)行解讀。后續(xù)指北君會(huì)根據(jù)小伙伴們需要對(duì)今天提及的知識(shí)點(diǎn)做深入的階段,比如invokeddynamic指令,class結(jié)構(gòu),動(dòng)態(tài)調(diào)用相關(guān)的各部分代碼邏輯。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 接口
    +關(guān)注

    關(guān)注

    33

    文章

    8448

    瀏覽量

    150724
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4284

    瀏覽量

    62325
  • 編譯
    +關(guān)注

    關(guān)注

    0

    文章

    649

    瀏覽量

    32775
  • JVM
    JVM
    +關(guān)注

    關(guān)注

    0

    文章

    157

    瀏覽量

    12197
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    如何查看及更改函數(shù)/函數(shù)塊的調(diào)用環(huán)境

    模塊化設(shè)計(jì)的思想是把一些相似的功能(比如電機(jī)控制、閥控制)設(shè)計(jì)成函數(shù)函數(shù)塊,這樣就可以反復(fù)調(diào)用。其優(yōu)點(diǎn)是:使程序架構(gòu)更加清晰,避免重復(fù)編寫相似功能的代碼。不過(guò)可能會(huì)產(chǎn)生一個(gè)疑惑:既然PLC的程序
    的頭像 發(fā)表于 11-17 09:08 ?839次閱讀
    如何查看及更改<b class='flag-5'>函數(shù)</b>/<b class='flag-5'>函數(shù)</b>塊的<b class='flag-5'>調(diào)用</b>環(huán)境

    Linux系統(tǒng)動(dòng)態(tài)庫(kù)與靜態(tài)庫(kù)函數(shù)的使用介紹

    ,N是庫(kù)的副版本號(hào)。當(dāng)然也可以不要版本號(hào),但名字必須有。相對(duì)于靜態(tài)函數(shù)庫(kù),動(dòng)態(tài)函數(shù)庫(kù)在編譯的時(shí)候并沒(méi)有被編譯進(jìn)目標(biāo)代碼中,你的程序執(zhí)行到相關(guān)函數(shù)時(shí)才調(diào)用
    發(fā)表于 07-04 05:33

    Linux下靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的制作與使用

    什么是靜態(tài)函數(shù)庫(kù)?動(dòng)態(tài)函數(shù)庫(kù)又是什么?linux靜態(tài)函數(shù)庫(kù)是怎樣創(chuàng)建并使用的?動(dòng)態(tài)函數(shù)庫(kù)是怎樣創(chuàng)建并使用的?
    發(fā)表于 04-26 06:45

    如何創(chuàng)建linux靜態(tài)函數(shù)庫(kù)?怎么使用?

    如何創(chuàng)建linux靜態(tài)函數(shù)庫(kù)?怎么使用?
    發(fā)表于 04-27 06:58

    C++教程之函數(shù)的遞歸調(diào)用

    C++教程之函數(shù)的遞歸調(diào)用 在執(zhí)行函數(shù) f 的過(guò)程中,又要調(diào)用 f 函數(shù)本身,稱為函數(shù)的遞歸
    發(fā)表于 05-15 18:00 ?35次下載

    動(dòng)態(tài)Feign的“萬(wàn)能”接口調(diào)用

    對(duì)于fegin調(diào)用,我們一般的用法都是為每個(gè)微服務(wù)都創(chuàng)建對(duì)應(yīng)的feignclient接口,然后為每個(gè)微服務(wù)的controller接口,一一編寫對(duì)應(yīng)的方法,去調(diào)用對(duì)應(yīng)微服務(wù)的
    發(fā)表于 12-26 11:42 ?3683次閱讀

    嵌入式軟件架構(gòu)設(shè)計(jì)之函數(shù)調(diào)用

    函數(shù)調(diào)用很好理解,即使剛學(xué)沒(méi)多久的朋友也知道函數(shù)調(diào)用是怎么實(shí)現(xiàn)的,即調(diào)用一個(gè)已經(jīng)封裝好的函數(shù),實(shí)
    的頭像 發(fā)表于 02-15 14:48 ?1054次閱讀
    嵌入式軟件架構(gòu)設(shè)計(jì)之<b class='flag-5'>函數(shù)</b><b class='flag-5'>調(diào)用</b>

    什么是函數(shù)調(diào)用?

    函數(shù)調(diào)用,就是使用我們已經(jīng)定義好的函數(shù),或者C語(yǔ)言自帶的庫(kù)函數(shù)。
    的頭像 發(fā)表于 04-04 17:21 ?5575次閱讀

    SCL中調(diào)用函數(shù)的示例

    在此,可插入函數(shù) (FC) 調(diào)用函數(shù)塊 (FB) 調(diào)用。函數(shù)塊可作為單實(shí)例、多重實(shí)例或參數(shù)實(shí)例進(jìn)行調(diào)用
    的頭像 發(fā)表于 06-06 10:18 ?2047次閱讀

    觸發(fā)器的輸出是現(xiàn)態(tài)函數(shù)

    觸發(fā)器的輸出是現(xiàn)態(tài)函數(shù) 觸發(fā)器是數(shù)字電路中的一種重要元件,它們通常被用于存儲(chǔ)和裝載二進(jìn)制數(shù)據(jù),也可以用于控制和同步各種數(shù)字電路。在許多數(shù)字電路應(yīng)用中,觸發(fā)器的輸出通常被用作輸入信號(hào)來(lái)觸發(fā)后續(xù)電路。在
    的頭像 發(fā)表于 08-24 15:50 ?937次閱讀

    Vivado ML版中動(dòng)態(tài)函數(shù)交換的技術(shù)進(jìn)步

    電子發(fā)燒友網(wǎng)站提供《Vivado ML版中動(dòng)態(tài)函數(shù)交換的技術(shù)進(jìn)步.pdf》資料免費(fèi)下載
    發(fā)表于 09-14 09:32 ?0次下載
    Vivado ML版中<b class='flag-5'>動(dòng)態(tài)函數(shù)</b>交換的技術(shù)進(jìn)步

    隔離設(shè)計(jì)流程+動(dòng)態(tài)函數(shù)交換示例

    電子發(fā)燒友網(wǎng)站提供《隔離設(shè)計(jì)流程+動(dòng)態(tài)函數(shù)交換示例.pdf》資料免費(fèi)下載
    發(fā)表于 09-14 09:31 ?0次下載
    隔離設(shè)計(jì)流程+<b class='flag-5'>動(dòng)態(tài)函數(shù)</b>交換示例

    使用抽象外殼進(jìn)行動(dòng)態(tài)函數(shù)交換的解決方案效率

    電子發(fā)燒友網(wǎng)站提供《使用抽象外殼進(jìn)行動(dòng)態(tài)函數(shù)交換的解決方案效率.pdf》資料免費(fèi)下載
    發(fā)表于 09-13 17:10 ?0次下載
    使用抽象外殼進(jìn)行<b class='flag-5'>動(dòng)態(tài)函數(shù)</b>交換的解決方案效率

    python定義函數(shù)調(diào)用函數(shù)的順序

    定義函數(shù)調(diào)用函數(shù)的順序 函數(shù)被定義后,本身是不會(huì)自動(dòng)執(zhí)行的,只有在被調(diào)用后,函數(shù)才會(huì)被執(zhí)行,得
    的頭像 發(fā)表于 10-04 17:17 ?1241次閱讀

    python函數(shù)函數(shù)之間的調(diào)用

    函數(shù)函數(shù)之間的調(diào)用 3.1 第一種情況 程序代碼如下: def x ( f ): def y (): print ( 1 ) return y def f (): print ( 2 )x(f
    的頭像 發(fā)表于 10-04 17:17 ?550次閱讀