Kotlin 協(xié)程學(xué)習(xí)總結(jié)

Kotlin 基礎(chǔ)精華篇
Kotlin 內(nèi)聯(lián)函數(shù)let、with、run乖订、apply、also
Kotlin 協(xié)程學(xué)習(xí)總結(jié)


一具练、協(xié)程的使用與說(shuō)明

        val job = GlobalScope.launch(
            context = Dispatchers.Default,
            start = CoroutineStart.DEFAULT,
            block = {
                val result1 = doTask("1", 2000)
                val result2 = async { doTask("1", 2000) }
                withContext(Dispatchers.Main) {
                    logMessage("result is ${result1}, ${result2.await()}")
                }
            }
        )

launch源碼:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
1乍构、CoroutineScope

協(xié)程范圍,即協(xié)程內(nèi)的代碼運(yùn)行的時(shí)間周期范圍扛点,如果超出了指定的協(xié)程范圍哥遮,協(xié)程會(huì)被取消執(zhí)行。

CoroutineScope
協(xié)程范圍接口
CoroutineContext:協(xié)程的上下文

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

ContextScope
協(xié)程范圍實(shí)現(xiàn)類

internal class ContextScope(context: CoroutineContext) : CoroutineScope

GlobalScope
全局作用域陵究,實(shí)現(xiàn)了 CoroutineScope接口 同時(shí)執(zhí)行了一個(gè)空的上下文對(duì)象的協(xié)程作用域眠饮。
其創(chuàng)建的協(xié)程沒有父協(xié)程,通常用于啟動(dòng)頂級(jí)協(xié)程铜邮,這些協(xié)程在整個(gè)應(yīng)用程序生命周期內(nèi)運(yùn)行仪召。
直接使用 GlobalScope 的 async 或者 launch 方法是強(qiáng)烈不建議的。程序代碼通常應(yīng)該使用自定義的協(xié)程作用域牲距。

MainScope
實(shí)現(xiàn)了 CoroutineScope接口 同時(shí)是通過調(diào)度器調(diào)度到了主線程的協(xié)程作用域

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

如:

class TestLifecycleActivity : AppCompatActivity(), CoroutineScope by MainScope() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.test_fargment)
        testCoroutine()
    }

    private fun testCoroutine() {
        logMessage("launchBefore")
        launch {
            //do task
            logMessage("taskBefore")
            val result1 = async { doTask("1", 20 * 1000) }
            val result2 = async { doTask("2", 20 * 1000) }
            val result3 = doTask("3", result1.await() + result2.await())
            logMessage("taskAfter")

            //切換主線程輸出結(jié)果
            withContext(Dispatchers.Main) {
                logMessage("result is ${result1.await()}, ${result2.await()}, $result3")
            }
        }
        logMessage("launchAfter")
    }

    override fun onDestroy() {
        super.onDestroy()
        logMessage("onDestroy - cancel")
        cancel()
    }

    //...
}
日志輸出:
    E/Coroutine: 16:49:50 983 *** ThreadName : main *** launchBefore
    E/Coroutine: 16:49:50 994 *** ThreadName : main *** launchAfter
    E/Coroutine: 16:49:51 243 *** ThreadName : main *** taskBefore
    E/Coroutine: 16:50:02 029 *** ThreadName : main *** onDestroy - cancel

自定義CoroutineScope

private val customScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

viewModelScope
添加如下依賴:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

使用:

class TestViewModel : androidx.lifecycle.ViewModel() {
    fun testCoroutine() {
        viewModelScope.launch {
            //do task
        }
    }
}

而viewModelScope是怎么定義的呢

/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

lifecycleScope
添加如下依賴:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

使用:

        lifecycleScope.launch {  }
        lifecycleScope.launchWhenCreated {  }
        lifecycleScope.launchWhenResumed {  }
        lifecycleScope.launchWhenStarted {  }

viewModelScope是怎么定義的呢


/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope


/**
 * [CoroutineScope] tied to this [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }
2返咱、launch

是CoroutineScope的一個(gè)擴(kuò)展函數(shù)。
默認(rèn)啟動(dòng)通過launch啟動(dòng)一個(gè)協(xié)程的時(shí)候包含一個(gè)繼承自作用域的CoroutineContext牍鞠,和一個(gè)默認(rèn)的啟動(dòng)模式,調(diào)度器和要執(zhí)行的協(xié)程體评姨,之后返回一個(gè)Job难述。同時(shí)內(nèi)部的job將成為外部job 的子job,當(dāng)一個(gè)父協(xié)程被取消的時(shí)候吐句,所有它的子協(xié)程也會(huì)被遞歸的取消胁后。

lauch 與 runBlocking 都能 開啟一個(gè)協(xié)程,但 lauch 是非阻塞的嗦枢,runBlocking 是阻塞的攀芯。

runBlocking中調(diào)用launch()會(huì)在當(dāng)前線程中執(zhí)行協(xié)程,也就是說(shuō)在runBlocking中不管開啟多少個(gè)子協(xié)程文虏,實(shí)際上它們都是使用runBlocking所在的線程執(zhí)行任務(wù)侣诺,所以會(huì)出現(xiàn)線程被霸占的情況殖演。

        runBlocking {
            GlobalScope.launch {
                //do task
                logMessage("taskBefore")
                val result1 = async { doTask("1", 1000) }
                val result2 = async { doTask("2", 1000) }
                val result3 = doTask("3", result1.await() + result2.await())
                logMessage("taskAfter")
            }
        }
日志輸出:(都在main線程順序執(zhí)行)
    E/Coroutine: 19:42:37 140 *** ThreadName : main *** launchBefore
    E/Coroutine: 19:42:37 157 *** ThreadName : main *** taskBefore
    E/Coroutine: 19:42:38 164 *** ThreadName : main *** task1 - 1000
    E/Coroutine: 19:42:38 166 *** ThreadName : main *** task2 - 1000
    E/Coroutine: 19:42:40 168 *** ThreadName : main *** task3 - 2000
    E/Coroutine: 19:42:40 168 *** ThreadName : main *** taskAfter
3、CoroutineContext

協(xié)程上下文年鸳,可以指定協(xié)程運(yùn)行的線程趴久。
默認(rèn)與指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默認(rèn)運(yùn)行在一個(gè)后臺(tái)工作線程內(nèi)搔确。也可以通過顯示指定參數(shù)來(lái)更改協(xié)程運(yùn)行的線程彼棍,Dispatchers提供了幾個(gè)值可以指定:Dispatchers.Default、Dispatchers.Main膳算、Dispatchers.IO座硕、Dispatchers.Unconfined。

Dispatchers.Default
CPU密集型任務(wù)涕蜂,如列表排序华匾、JSON轉(zhuǎn)換等

Dispatchers.Main
主線程,和UI交互宇葱,執(zhí)行輕量任務(wù)

Dispatchers.IO
常用于網(wǎng)絡(luò)請(qǐng)求和文件訪問

Dispatchers.Unconfined
不限制任何制定線程瘦真,一般不用

4、CoroutineStart

協(xié)程的啟動(dòng)模式黍瞧。
默認(rèn)CoroutineStart.DEFAULT是指協(xié)程立即執(zhí)行诸尽,除此之外還有CoroutineStart.LAZY、CoroutineStart.ATOMIC印颤、CoroutineStart.UNDISPATCHED您机。

CoroutineStart.DEFAULT
立即執(zhí)行協(xié)程體

CoroutineStart.LAZY
懶漢式啟動(dòng)。launch后并不會(huì)有任何調(diào)度行為年局,協(xié)程體也不會(huì)進(jìn)入執(zhí)行狀態(tài)际看,直到需要它執(zhí)行的時(shí)候,即需要它的運(yùn)行結(jié)果的時(shí)候矢否,launch調(diào)用后會(huì)返回一個(gè) Job實(shí)例:
調(diào)用 Job.start仲闽,主動(dòng)觸發(fā)協(xié)程的調(diào)度執(zhí)行
調(diào)用 Job.join,隱式的觸發(fā)協(xié)程的調(diào)度執(zhí)行

CoroutineStart.ATOMIC
立即執(zhí)行協(xié)程體僵朗,但在開始運(yùn)行后無(wú)法取消赖欣,無(wú)視 job.cancel()

CoroutineStart.UNDISPATCHED
立即在當(dāng)前線程執(zhí)行協(xié)程體,直到第一個(gè) suspend 調(diào)用验庙。
協(xié)程在這種模式下會(huì)直接開始在當(dāng)前線程下執(zhí)行顶吮,直到第一個(gè)掛起點(diǎn),這聽起來(lái)有點(diǎn)兒像前面的 ATOMIC粪薛,不同之處在于 UNDISPATCHED 不經(jīng)過任何調(diào)度器即開始執(zhí)行協(xié)程體悴了。當(dāng)然遇到掛起點(diǎn)之后的執(zhí)行就取決于掛起點(diǎn)本身的邏輯以及上下文當(dāng)中的調(diào)度器了。

5、block

協(xié)程主體湃交,也就是要在協(xié)程內(nèi)部運(yùn)行的代碼熟空。

6、Job

返回值巡揍,對(duì)當(dāng)前創(chuàng)建的協(xié)程的引用痛阻。可以通過Job的start腮敌、cancel阱当、join等方法來(lái)控制協(xié)程的啟動(dòng)和取消。

isActive 協(xié)程是否存活(注意懶啟動(dòng))
isCancelled 協(xié)程是否取消
isCompleted 協(xié)程是否完成
cancel() 取消協(xié)程
start() 啟動(dòng)協(xié)程
join() 阻塞等候協(xié)程完成
cancelAndJoin() 取消并等候協(xié)程完成handler: CompletionHandler) 監(jiān)聽協(xié)程的狀態(tài)回調(diào)
attachChild(child: ChildJob) 附加一個(gè)子協(xié)程到當(dāng)前協(xié)程上
7糜工、async弊添、await

是CoroutineScope的一個(gè)擴(kuò)展函數(shù),用于開啟一個(gè)新的子協(xié)程捌木,與 launch 函數(shù)一樣可以設(shè)置啟動(dòng)模式镣丑,不同的是它的返回值為 Deferred章喉,Deferred是Job的子類唬滑,但是通過Deferred.await()可以得到一個(gè)返回值它掂。

await() 只有在 async 未執(zhí)行完成返回結(jié)果時(shí),才會(huì)掛起協(xié)程帆啃。若 async 已經(jīng)有結(jié)果了瞬女,await() 則直接獲取其結(jié)果并賦值給變量,此時(shí)不會(huì)掛起協(xié)程努潘。

8诽偷、suspend

suspend fun 掛起函數(shù),即該函數(shù)是一個(gè)耗時(shí)操作疯坤,須放在協(xié)程中執(zhí)行报慕。

掛起函數(shù)只能在協(xié)程中和其他掛起函數(shù)中調(diào)用,不能在其他地方使用压怠。

suspend函數(shù)會(huì)將整個(gè)協(xié)程掛起眠冈,而不僅僅是這個(gè)suspend函數(shù),也就是說(shuō)一個(gè)協(xié)程中有多個(gè)掛起函數(shù)時(shí)菌瘫,它們是順序執(zhí)行的洋闽。

9、withContext

withContext 與 async 都可以返回耗時(shí)任務(wù)的執(zhí)行結(jié)果突梦。 通常也會(huì)使用withContext實(shí)現(xiàn)線程切換。

一般來(lái)說(shuō)羽利,多個(gè) withContext 任務(wù)是串行的宫患, 且withContext 可直接返回耗時(shí)任務(wù)的結(jié)果。 多個(gè) async 任務(wù)是并行的这弧,async 返回的是一個(gè)Deferred<T>娃闲,需要調(diào)用其await()方法獲取結(jié)果虚汛。

二、協(xié)程的執(zhí)行測(cè)試

兩個(gè)輔助方法

    private suspend fun doTask(msg: String, delayTime: Long = 0) =
        (if (delayTime < 1000) (1000 + Math.random() * 1000).toLong() else delayTime).apply {
            //延時(shí)
            delay(this)
            logMessage("task$msg - $this")
        }

    private fun logMessage(msg: String) {
        currentTime.time = System.currentTimeMillis()
        //日志輸出當(dāng)前時(shí)間皇帮、線程名卷哩、自定義信息
        Log.e(
            "Coroutine",
            "${dateFormat.format(currentTime)} *** ThreadName : ${Thread.currentThread().name} *** $msg"
        )
    }
1、順序執(zhí)行:task3依賴task2的結(jié)果属拾,task2依賴task1的結(jié)果
    private fun testCoroutine() {
        logMessage("launchBefore")
        GlobalScope.launch {
            //do task
            logMessage("taskBefore")
            val result1 = doTask("1")
            val result2 = doTask("2", result1 - 1000)
            val result3 = doTask("3", result2 - 1000)
            logMessage("taskAfter")
            //輸出結(jié)果
            withContext(Dispatchers.Main) {
                logMessage("result is ${result1}, ${result2}, $result3")
            }
        }
        logMessage("launchAfter")
    }
日志輸出:
    E/Coroutine: 11:26:57 530 *** ThreadName : main *** launchBefore
    E/Coroutine: 11:26:57 590 *** ThreadName : main *** launchAfter
    E/Coroutine: 11:26:57 591 *** ThreadName : DefaultDispatcher-worker-1 *** taskBefore
    E/Coroutine: 11:26:59 475 *** ThreadName : DefaultDispatcher-worker-1 *** task1 - 1876
    E/Coroutine: 11:27:01 413 *** ThreadName : DefaultDispatcher-worker-1 *** task2 - 1936
    E/Coroutine: 11:27:03 386 *** ThreadName : DefaultDispatcher-worker-1 *** task3 - 1971
    E/Coroutine: 11:27:03 386 *** ThreadName : DefaultDispatcher-worker-1 *** taskAfter
    E/Coroutine: 11:27:03 394 *** ThreadName : main *** result is 1876, 1936, 1971

由以上日志輸出可知:

  1. 協(xié)程 launch 中執(zhí)行耗時(shí)任務(wù)沒有阻塞主線程(launchAfter先于協(xié)程輸出)
  2. task1将谊、task2、task3 順序執(zhí)行 (執(zhí)行時(shí)間時(shí)間線)
2渐白、異步執(zhí)行:task3依賴task1尊浓、task2的結(jié)果,task1纯衍、task2異步執(zhí)行
    private fun testCoroutine() {
        logMessage("launchBefore")
        GlobalScope.launch {
            //do task
            logMessage("taskBefore")
            val result1 = async { doTask("1", 2000) }
            val result2 = async { doTask("2", 2000) }
            val result3 = doTask("3", result1.await() + result2.await())
            logMessage("taskAfter")
            //輸出結(jié)果
            withContext(Dispatchers.Main) {
                logMessage("result is ${result1.await()}, ${result2.await()}, $result3")
            }
        }
        logMessage("launchAfter")
    }
日志輸出:
    E/Coroutine: 12:22:50 202 *** ThreadName : main *** launchBefore
    E/Coroutine: 12:22:50 264 *** ThreadName : main *** launchAfter
    E/Coroutine: 12:22:50 266 *** ThreadName : DefaultDispatcher-worker-1 *** taskBefore
    E/Coroutine: 12:22:52 279 *** ThreadName : DefaultDispatcher-worker-2 *** task1 - 2000
    E/Coroutine: 12:22:52 279 *** ThreadName : DefaultDispatcher-worker-3 *** task2 - 2000
    E/Coroutine: 12:22:56 282 *** ThreadName : DefaultDispatcher-worker-2 *** task3 - 4000
    E/Coroutine: 12:22:56 283 *** ThreadName : DefaultDispatcher-worker-2 *** taskAfter
    E/Coroutine: 12:22:56 290 *** ThreadName : main *** result is 2000, 2000, 4000

async { } 異步執(zhí)行
await() 獲取異步執(zhí)行結(jié)果



更多資料參考:
將 Kotlin 協(xié)程與架構(gòu)組件一起使用
超長(zhǎng)文栋齿,帶你全面了解Kotlin的協(xié)程
Kotlin協(xié)程核心庫(kù)分析
Kotlin實(shí)戰(zhàn)指南十四:協(xié)程啟動(dòng)模式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市襟诸,隨后出現(xiàn)的幾起案子瓦堵,更是在濱河造成了極大的恐慌,老刑警劉巖歌亲,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件菇用,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡应结,警方通過查閱死者的電腦和手機(jī)刨疼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鹅龄,“玉大人揩慕,你說(shuō)我怎么就攤上這事“缧荩” “怎么了迎卤?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)玷坠。 經(jīng)常有香客問我蜗搔,道長(zhǎng),這世上最難降的妖魔是什么八堡? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任樟凄,我火速辦了婚禮,結(jié)果婚禮上兄渺,老公的妹妹穿的比我還像新娘缝龄。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布叔壤。 她就那樣靜靜地躺著瞎饲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪炼绘。 梳的紋絲不亂的頭發(fā)上嗅战,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音俺亮,去河邊找鬼驮捍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛铅辞,可吹牛的內(nèi)容都是我干的厌漂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼斟珊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼苇倡!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起囤踩,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤旨椒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后堵漱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體综慎,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年勤庐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了示惊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡愉镰,死狀恐怖米罚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丈探,我是刑警寧澤录择,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站碗降,受9級(jí)特大地震影響隘竭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜讼渊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一动看、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爪幻,春花似錦弧圆、人聲如沸赋兵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至叶组,卻和暖如春拯田,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背甩十。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工船庇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侣监。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓鸭轮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親橄霉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窃爷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354