Android組件化開(kāi)發(fā)實(shí)踐(十):通過(guò)Gradle插件統(tǒng)一規(guī)范

相信大部分的開(kāi)發(fā)團(tuán)隊(duì),不管前端也好,后端也好,都會(huì)有自己內(nèi)部的一套規(guī)范。它是團(tuán)隊(duì)協(xié)作開(kāi)發(fā)的基石,如果團(tuán)隊(duì)成員各自搞自己的,最后集成時(shí)肯定或多或少會(huì)出現(xiàn)問(wèn)題。所以問(wèn)題就來(lái)了,在我們組件化開(kāi)發(fā)的過(guò)程中,每個(gè)人各自開(kāi)發(fā)自己的組件,單獨(dú)運(yùn)行時(shí)可能沒(méi)問(wèn)題,但是最后集成打包時(shí)總是失敗。作為一個(gè)合格的團(tuán)隊(duì) leader ,你肯定強(qiáng)調(diào)過(guò)各組員要遵循一致的代碼規(guī)范、行為準(zhǔn)則等,甚至形成各種必要的規(guī)范文檔。但是實(shí)踐告訴我們,這需要所有人都要有很強(qiáng)的自覺(jué)性,但是這種靠自覺(jué)性的規(guī)則往往是靠不住的,你不能保證所有人都理解了你的規(guī)則,也不能保證所有人每時(shí)每刻都按照這個(gè)規(guī)則來(lái)執(zhí)行,如果沒(méi)有強(qiáng)有力的執(zhí)行,這個(gè)規(guī)則就是一紙空文,很快就會(huì)被淡忘。基于這個(gè)原因,在組件化開(kāi)發(fā)的過(guò)程中,我們可以通過(guò)自定義 Gradle 插件的方式,來(lái)統(tǒng)一各種規(guī)范,以下講講我在這方面的部分實(shí)踐(這需要了解 Gradle 相關(guān)知識(shí))。

1. 統(tǒng)一compileSdkVersion、minSdkVersion、targetSdkVersion

每個(gè)人的開(kāi)發(fā)環(huán)境都是不相同的,編譯環(huán)境的不同的可能會(huì)導(dǎo)致編譯結(jié)果的差異。舉幾個(gè)栗子:當(dāng) targetSdkVersion >= 23 時(shí),安卓引入了動(dòng)態(tài)權(quán)限,所有敏感權(quán)限都需要先申請(qǐng)?jiān)偈褂茫?targetSdkVersion < 23 時(shí),是不需要申請(qǐng)的,如果有的人使用了低版本 sdk ,那么最終集成到主 app 中時(shí),就可能會(huì)出現(xiàn)權(quán)限方面的問(wèn)題了;其次就是支持的最小 sdk 版本問(wèn)題了,由于歷史原因,很多 api 在高版本 sdk 中才出現(xiàn),如果有的人在開(kāi)發(fā)組件的過(guò)程中設(shè)置的 minSdkVersion = 23,但為了兼容更多的手機(jī),集成打包時(shí)設(shè)置的 minSdkVersion = 19,那打包就會(huì)出現(xiàn)問(wèn)題,或者是在低版本系統(tǒng)的手機(jī)上不兼容出現(xiàn)閃退。

通過(guò)插件強(qiáng)制使用相同的 sdk 版本:

static def MIN_SDK = 19
static def TARGET_SDK = 26
static def COMPILE_SDK = "android-26"

project.afterEvaluate {
    com.android.build.gradle.BaseExtension android = project.extensions.getByName("android")

    //強(qiáng)制統(tǒng)一 compileSdkVersion、 minSdkVersion、targetSdkVersion
    String compileSdkVersion = android.compileSdkVersion
    int targetSdkVersion = android.defaultConfig.targetSdkVersion.apiLevel
    int minSdkVersion = android.defaultConfig.minSdkVersion.apiLevel
    if (compileSdkVersion != COMPILE_SDK) {
        throw new GradleException("請(qǐng)修改 compileSdkVersion,必須設(shè)置為 ${COMPILE_SDK}")
    }
    if (minSdkVersion != MIN_SDK) {
        throw new GradleException("請(qǐng)修改 minSdkVersion,必須設(shè)置為 ${MIN_SDK}")
    }
    if (targetSdkVersion != TARGET_SDK) {
        throw new GradleException("請(qǐng)修改 targetSdkVersion,必須設(shè)置為 ${TARGET_SDK}")
    }
}

如果發(fā)現(xiàn) sdk 版本不一致,直接拋出異常,強(qiáng)制所有人使用相同的 sdk 版本。

2. 統(tǒng)一 support 等常用第三方庫(kù)的版本

由于 support 庫(kù)使用范圍實(shí)在太廣了,不僅我們自己會(huì)使用到,很多第三方庫(kù)也可能會(huì)依賴(lài)到,最終會(huì)出現(xiàn)各種不同的版本號(hào),以我自己的一個(gè)項(xiàng)目為例:

support庫(kù)沖突

除了 support 庫(kù)之外,還有很多其他的常用庫(kù),例如:okhttp、retrofit、gson 等,我們可以采用 gradle 的解析策略來(lái)強(qiáng)制統(tǒng)一版本號(hào):

static def SUPPORT_VERSION = "26.1.0"
static def MULTIDEX_VERSION = "1.0.2"
static def GSON_VERSION = "2.8.0"
static def KOTLIN_VERSION = "1.3.40"

ConfigurationContainer container = project.configurations
container.all { Configuration conf ->
    ResolutionStrategy rs = conf.resolutionStrategy
    rs.force 'com.google.code.findbugs:jsr305:2.0.1'
    //統(tǒng)一第三方庫(kù)的版本號(hào)
    rs.eachDependency { details ->
        def requested = details.requested
        if (requested.group == "com.android.support") {
            //強(qiáng)制所有的 com.android.support 庫(kù)采用固定版本
            if (requested.name.startsWith("multidex")) {
                details.useVersion(MULTIDEX_VERSION)
            } else {
                details.useVersion(SUPPORT_VERSION)
            }
        } else if (requested.group == "com.google.code.gson") {
            //統(tǒng)一 Gson 庫(kù)的版本號(hào)
            details.useVersion(GSON_VERSION)
        } else if (requested.group == "org.jetbrains.kotlin") {
            //統(tǒng)一內(nèi)部 kotlin 庫(kù)的版本
            details.useVersion(KOTLIN_VERSION)
        }
    }
}

在實(shí)踐過(guò)程中,可以逐漸收集常用的第三方庫(kù),定時(shí)更新版本號(hào)。

3. 統(tǒng)一添加 git hook

什么是 git hook 呢?簡(jiǎn)單說(shuō)來(lái),就是 git 鉤子,當(dāng)我們采用 git 管理代碼時(shí),提交代碼、更新代碼、回退代碼等等操作時(shí),會(huì)先觸發(fā)一個(gè)腳本執(zhí)行。基于這個(gè)功能,我們可以做很多事情,比如:檢查 commit 的信息是否規(guī)范,不規(guī)范的信息不允許提交;push 代碼時(shí),先做個(gè) lint 檢查,有問(wèn)題或不符合規(guī)范的代碼禁止推到遠(yuǎn)程分支上。

使用 git 管理代碼時(shí),在工程根目錄下,會(huì)默認(rèn)有個(gè) .git/hooks 目錄,我們看看這個(gè)目錄下都有些什么文件,如下圖所示:

.git/hooks 目錄文件

可以看到有很多以.sample為后綴名的文件,這些都是 git hook 文件,默認(rèn)情況下 git hook 是不開(kāi)啟的,但是當(dāng)去掉 .sample 后綴時(shí),對(duì)應(yīng)的 hook 就生效了。以commit-msg.sample為例,我們將之重命名為commit-msg,當(dāng)我們執(zhí)行git commit命令時(shí),會(huì)先執(zhí)行該腳本文件,如果腳本運(yùn)行通過(guò),commit 才會(huì)成功,否則就會(huì)提交失敗。除此之外,其他的功能就不一一贅述了,可搜索相應(yīng)資料進(jìn)行學(xué)習(xí)。

很顯然,我們不能要求所有人都能自覺(jué)地配置 git hook,這樣太繁瑣了,如果能通過(guò)插件自動(dòng)為我們配置一切,那是不是就完美了。例如:我們想通過(guò) git hook 規(guī)范所有人的 commit 信息,其思路如下:

  1. 首先檢測(cè) .git/hooks/commit-msg 文件是否存在;
  2. 如果已存在則不處理;
  3. 如果不存在,則將 .git/hooks/commit-msg.sample 文件重命名為 commit-msg;
  4. 將要檢測(cè)提交信息是否規(guī)范的腳本代碼寫(xiě)入 commit-msg 文件里;
private static final String GIT_COMMIT_MSG_CONFIG = '''#!/usr/bin/env groovy
import static java.lang.System.exit

//要提交的信息保存在該文件里
def commitMsgFileName = args[0]
def msgFile = new File(commitMsgFileName)
//讀出里面的提交信息
def commitMsg = msgFile.text

//對(duì)要提交的信息做校驗(yàn),如果不符合要求的,不允許提交
def reg = ~"^(fix:|add:|update:|refactor:|perf:|style:|test:|docs:|revert:|build:)[\\\\w\\\\W]{5,100}"
if (!commitMsg.matches(reg)) {
    StringBuilder sb = new StringBuilder()
    sb.append("================= Commit Error =================\\n")
    sb.append("===>Commit 信息不規(guī)范,描述信息字?jǐn)?shù)范圍為[5, 100],具體格式請(qǐng)按照以下規(guī)范:\\n")
    sb.append("    fix: 修復(fù)某某bug\\n")
    sb.append("    add: 增加了新功能\\n")
    sb.append("    update: 更新某某功能\\n")
    sb.append("    refactor: 某個(gè)已有功能重構(gòu)\\n")
    sb.append("    perf: 性能優(yōu)化\\n")
    sb.append("    style: 代碼格式改變\\n")
    sb.append("    test: 增加測(cè)試代碼\\n")
    sb.append("    docs: 文檔改變\\n")
    sb.append("    revert: 撤銷(xiāo)上一次的commit\\n")
    sb.append("    build: 構(gòu)建工具或構(gòu)建過(guò)程等的變動(dòng)\\n")
    sb.append("================================================")
    println(sb.toString())
    exit(1)    
}

exit(0)
'''

//在根目錄的 .git/hooks 目錄下,存在很多 .sample 文件,把相應(yīng)的 .sample 后綴去掉,git hook 就生效了
File rootDir = project.rootProject.getProjectDir()
File gitHookDir = new File(rootDir, ".git/hooks")

//如果該目錄存在
if (gitHookDir.exists()) {
    //將 commit-msg.sample 文件的后綴名去掉,git hook 就會(huì)生效
    File commitMsgSampleFile = new File(gitHookDir, "commit-msg.sample")
    File commitMsgFile = new File(gitHookDir, "commit-msg")
    if (!commitMsgFile.exists() && commitMsgSampleFile.exists()) {
        //重命名的方式,自己創(chuàng)建的文件可能沒(méi)有可執(zhí)行權(quán)限,需要手動(dòng)加權(quán)限,故采用重命名原文件的方式,省去手動(dòng)加權(quán)限的操作
        commitMsgSampleFile.renameTo(commitMsgFile)
        commitMsgFile.setText(GIT_COMMIT_MSG_CONFIG)
        println("-----自動(dòng)配置 git hook 成功-----")
    } else {
        println("-----git hook 已經(jīng)啟用-----")
    }
} else {
    println("-----沒(méi)有找到.git目錄----")
}

提交信息規(guī)范參考了網(wǎng)上別人的文章,可以定制符合自己團(tuán)隊(duì)需求的規(guī)范。這里的腳本文件,我是采用 groovy 來(lái)實(shí)現(xiàn)的,因此需要預(yù)先安裝 groovy 運(yùn)行環(huán)境。比較好的方案是直接使用 shell 腳本,但我對(duì)此不是特別熟練,還有就是這里不支持 windows 運(yùn)行環(huán)境,如需支持還得額外考慮(當(dāng)然我們默認(rèn)開(kāi)發(fā)人員都是用 mac 的)。里面有個(gè)地方需要特別注意,commit-msg 文件一定要有可執(zhí)行權(quán)限,如果是代碼創(chuàng)建,是沒(méi)有可執(zhí)行權(quán)限的,所以我這里采用的是將 commit-msg.sample 文件重命名為 commit-msg 的方式,這樣就避免了還要額外手動(dòng)增加權(quán)限的步驟,真正做到了自動(dòng)化增加 git hook 的功能。

4. ProGuard 規(guī)則限制

這個(gè)是受“知乎APP”組件化方案的啟發(fā):“aar 中可以攜帶 ProGuard 規(guī)則,理論上來(lái)說(shuō),開(kāi)發(fā)同學(xué)可以在自己組件中任意添加 ProGuard 規(guī)則并影響到主工程”。如果有人不小心這樣配置:

-ignorewarnings
-dontwarn **
-keep class com.xx.** { *;}

這樣將會(huì)產(chǎn)生很大的影響:一是盲目 keep 導(dǎo)致很多代碼無(wú)法混淆壓縮;二是盲目 dontwarn,導(dǎo)致很多警告被忽略無(wú)法發(fā)現(xiàn),后果不堪設(shè)想。通過(guò)插件在編譯時(shí)讀取 ProGuard 配置文件,發(fā)現(xiàn)有不合規(guī)的配置,則直接終止打包,具體的檢測(cè)規(guī)則有:

  1. 禁止使用 -ignorewarnings
  2. 禁止使用 -dontwarn **
  3. 包含我們業(yè)務(wù)的包名,限制 dontwarn 的范圍,例如我們某個(gè)業(yè)務(wù)包名為 com.hjy.app,則禁止使用 -dontwarn com.hjy.app.**
  4. 禁止使用 -keep class **,這樣一把梭太危險(xiǎn)了;
  5. 同樣限制 keep 的范圍,禁止使用類(lèi)似 -kepp class com.hjy.app.* { *; },這樣包含的范圍太廣了;
  6. 禁止使用 -dontshrink-dontoptimize,這是關(guān)于壓縮性能優(yōu)化的選項(xiàng);

很多時(shí)候,我們?cè)谑褂玫谌揭蕾?lài)庫(kù)時(shí),有些會(huì)要求你一把梭全部無(wú)腦 keep,通過(guò)插件自動(dòng)檢測(cè)的方式,可以避免最終打包時(shí)采用了這些無(wú)腦的規(guī)則。

5. 打包選項(xiàng)自動(dòng)移除不必要文件

我曾經(jīng)在用 Kotlin 開(kāi)發(fā)的過(guò)程中,會(huì)發(fā)現(xiàn)打出的 aar 會(huì)包含一個(gè)類(lèi)似 META-INF/library_release.kotlin_module的文件,當(dāng)我集成打包時(shí),發(fā)現(xiàn)不同的 aar 包中含有相同的 .kotlin_module 文件,這樣會(huì)導(dǎo)致打包失敗,這個(gè)時(shí)候通常的做法是在 build.gradle 文件中這樣配置:

packagingOptions {
    exclude 'META-INF/*.kotlin_module'
}

這完全可以在插件中自動(dòng)實(shí)現(xiàn),避免手動(dòng)配置:

project.afterEvaluate {
    com.android.build.gradle.BaseExtension android = project.extensions.getByName("android")
    android.getPackagingOptions().exclude("META-INF/*.kotlin_module")
}

6. configuration 沖突

在配置依賴(lài)時(shí),可以使用 copile、implementation、api 等等,其中 api 是 compile 的升級(jí)方式,功能基本一樣。現(xiàn)在官方一直推薦使用 implementation ,它與 api 的核心區(qū)別是做了一些依賴(lài)隔離。舉個(gè)栗子:如果一個(gè)依賴(lài)鏈?zhǔn)沁@樣的:A -> B -> C,當(dāng)采用 implementation 的方式依賴(lài)時(shí),A 是不能直接訪問(wèn) C 的。但是在實(shí)際使用過(guò)程中,發(fā)現(xiàn)使用 implementation 并沒(méi)有帶來(lái)很大的收益,反而帶來(lái)很多問(wèn)題,因此可以使用插件將 implementation 轉(zhuǎn)換成 compile 或 api ,以后也不用關(guān)心它們的差別了。

7. 其他

除此之外,通過(guò)插件還可以做更多事情:

  1. 強(qiáng)制 lint,在代碼發(fā)布前必須強(qiáng)制運(yùn)行 lint;
  2. 限制第三方庫(kù)的無(wú)節(jié)制引入,例如防止引入多個(gè)不同的圖片加載框架;
  3. 檢查重復(fù)資源等;

8. 插件使用

部分代碼已經(jīng)開(kāi)源,github 地址:https://github.com/houjinyun/android-comm-config-plugin

系列文章
Android組件化開(kāi)發(fā)實(shí)踐(一):為什么要進(jìn)行組件化開(kāi)發(fā)?
Android組件化開(kāi)發(fā)實(shí)踐(二):組件化架構(gòu)設(shè)計(jì)
Android組件化開(kāi)發(fā)實(shí)踐(三):組件開(kāi)發(fā)規(guī)范
Android組件化開(kāi)發(fā)實(shí)踐(四):組件間通信問(wèn)題
Android組件化開(kāi)發(fā)實(shí)踐(五):組件生命周期管理
Android組件化開(kāi)發(fā)實(shí)踐(六):老項(xiàng)目實(shí)施組件化
Android組件化開(kāi)發(fā)實(shí)踐(七):開(kāi)發(fā)常見(jiàn)問(wèn)題及解決方案
Android組件化開(kāi)發(fā)實(shí)踐(八):組件生命周期如何實(shí)現(xiàn)自動(dòng)注冊(cè)管理
Android組件化開(kāi)發(fā)實(shí)踐(九):自定義Gradle插件
Android組件化開(kāi)發(fā)實(shí)踐(十):通過(guò)Gradle插件統(tǒng)一規(guī)范

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,407評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,981評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,759評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,204評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,415評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,650評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,892評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,675評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評(píng)論 2 374

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

  • 本文緊接著前一章Android組件化開(kāi)發(fā)實(shí)踐(八):組件生命周期如何實(shí)現(xiàn)自動(dòng)注冊(cè)管理,主要講解怎么通過(guò)自定義插件來(lái)...
    云飛揚(yáng)1閱讀 14,598評(píng)論 16 77
  • 項(xiàng)目發(fā)展到一定程度,就必須進(jìn)行模塊的拆分。模塊化是一種指導(dǎo)理念,其核心思想就是分而治之、降低耦合。而在 Andro...
    69451dd36574閱讀 2,440評(píng)論 0 3
  • 不以規(guī)矩,不成方圓。特別是多人協(xié)作開(kāi)發(fā)時(shí),如果沒(méi)有統(tǒng)一的開(kāi)發(fā)規(guī)范,勢(shì)必會(huì)造成各種混亂。在實(shí)際開(kāi)發(fā)中,常常會(huì)碰到的問(wèn)...
    云飛揚(yáng)1閱讀 13,266評(píng)論 22 64
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過(guò)就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 125,539評(píng)論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn),也就放棄了無(wú)數(shù)的可能。 ...
    yichen大刀閱讀 6,076評(píng)論 0 4