android實(shí)現(xiàn)多線程下載文件(支持暫停、取消、斷點(diǎn)續(xù)傳)
多線程下載文件(支持暫停、取消、斷點(diǎn)續(xù)傳)
多線程同時(shí)下載文件即:在同一時(shí)間內(nèi)通過(guò)多個(gè)線程對(duì)同一個(gè)請(qǐng)求地址發(fā)起多個(gè)請(qǐng)求,將需要下載的數(shù)據(jù)分割成多個(gè)部分,同時(shí)下載,每個(gè)線程只負(fù)責(zé)下載其中的一部分,最后將每一個(gè)線程下載的部分組裝起來(lái)即可。
涉及的知識(shí)及問(wèn)題
- 請(qǐng)求的數(shù)據(jù)如何分段
- 分段完成后如何下載和下載完成后如何組裝到一起
- 暫停下載和繼續(xù)下載的實(shí)現(xiàn)(wait()、notifyAll()、synchronized的使用)
- 取消下載和斷點(diǎn)續(xù)傳的實(shí)現(xiàn)
一、請(qǐng)求的數(shù)據(jù)如何分段
首先通過(guò)HttpURLConnection請(qǐng)求總文件大小,而后根據(jù)線程數(shù)計(jì)算每一個(gè)線程的下載量,在分配給每一個(gè)線程去下載
fileLength = conn.getContentLength(); //根據(jù)文件大小,先創(chuàng)建一個(gè)空文件 //“r“——以只讀方式打開(kāi)。調(diào)用結(jié)果對(duì)象的任何 write 方法都將導(dǎo)致拋出 IOException。 //“rw“——打開(kāi)以便讀取和寫(xiě)入。如果該文件尚不存在,則嘗試創(chuàng)建該文件。 //“rws“—— 打開(kāi)以便讀取和寫(xiě)入,對(duì)于 “rw”,還要求對(duì)文件的內(nèi)容或元數(shù)據(jù)的每個(gè)更新都同步寫(xiě)入到底層存儲(chǔ)設(shè)備。 //“rwd“——打開(kāi)以便讀取和寫(xiě)入,對(duì)于 “rw”,還要求對(duì)文件內(nèi)容的每個(gè)更新都同步寫(xiě)入到底層存儲(chǔ)設(shè)備。 RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.setLength(fileLength); raf.close(); //計(jì)算各個(gè)線程下載的數(shù)據(jù)段 int blockLength = fileLength / threadCount;
二、分段完成后如何下載和下載完成后如何組裝到一起
分段完成后給每一個(gè)線程的請(qǐng)求頭設(shè)置Range參數(shù),他允許客戶(hù)端只請(qǐng)求文件的一部分?jǐn)?shù)據(jù),每一個(gè)線程只請(qǐng)求下載相應(yīng)范圍內(nèi)的數(shù)據(jù),使用RandomAccessFile(可隨機(jī)讀寫(xiě)的文件)寫(xiě)入到同一個(gè)文件里即可組裝成目標(biāo)文件Range,是在 HTTP/1.1里新增的一個(gè) header field,它允許客戶(hù)端實(shí)際上只請(qǐng)求文檔的一部分(范圍可以相互重疊)
Range的使用形式:
屬性 | 解釋 |
---|---|
bytes=0-499 | 表示頭500個(gè)字節(jié) |
bytes=500-999 | 表示第二個(gè)500字節(jié) |
bytes=-500 | 表示最后500個(gè)字節(jié) |
bytes=500- | 表示500字節(jié)以后的范圍 |
bytes=0-0,-1 | 第一個(gè)和最后一個(gè)字節(jié) |
HttpUrlConnection中設(shè)置請(qǐng)求頭
URL url = new URL(loadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); conn.setConnectTimeout(5000); //若請(qǐng)求頭加上Range這個(gè)參數(shù),則返回狀態(tài)碼為206,而不是200 if (conn.getResponseCode() == 206) { InputStream is = conn.getInputStream(); RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.seek(startPosition);//跳到指定位置開(kāi)始寫(xiě)數(shù)據(jù) }
三、暫停下載和繼續(xù)下載的實(shí)現(xiàn)(wait()、notifyAll()、synchronized的使用)
關(guān)于synchronized只需記住一下五點(diǎn):
- 當(dāng)兩個(gè)并發(fā)線程訪問(wèn)同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí),一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
- 然而,當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問(wèn)該object中的非synchronized(this)同步代碼塊。
- 尤其關(guān)鍵的是,當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其它synchronized(this)同步代碼塊的訪問(wèn)將被阻塞。
- 第三個(gè)例子同樣適用其它同步代碼塊。也就是說(shuō),當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖。結(jié)果,其它線程對(duì)該object對(duì)象所有同步代碼部分的訪問(wèn)都被暫時(shí)阻塞。
- 以上規(guī)則對(duì)其它對(duì)象鎖同樣適用.
protected void onPause() { if (mThreads != null) stateDownload = DOWNLOAD_PAUSE; } protected void onStart() { if (mThreads != null) synchronized (DOWNLOAD_PAUSE) { stateDownload = DOWNLOAD_ING; DOWNLOAD_PAUSE.notifyAll(); } }
對(duì)于wait()、notify()、notifyAll()需要注意的是
- 調(diào)用任何對(duì)象的wait()方法時(shí),都必須先獲得該對(duì)象的鎖,即調(diào)用的wait()方法必須得寫(xiě)在synchronized(obj){…}之內(nèi)
- 當(dāng)調(diào)用對(duì)象的wait()方法后,該線程若想繼續(xù)執(zhí)行,必須得再次獲得該對(duì)象的鎖才可以
- 如果A1,A2,A3線程都在obj.wait(),則B調(diào)用object.notify()只能喚醒A1,A2,A3中的一個(gè)(具體哪一個(gè)由JVM決定)
- 當(dāng)B調(diào)用object.notify/notifyAll的時(shí)候,B正持有object鎖,因此,A1,A2,A3雖被喚醒,但是仍無(wú)法獲得object鎖直到B退出synchronized塊,釋放object鎖后,A1,A2,A3中的一個(gè)/全部才有機(jī)會(huì)獲得鎖繼續(xù)執(zhí)行
synchronized (DOWNLOAD_PAUSE) { if (stateDownload.equals(DOWNLOAD_PAUSE)) { DOWNLOAD_PAUSE.wait(); } }
四、取消下載和斷點(diǎn)續(xù)傳的實(shí)現(xiàn)
取消下載即取消每個(gè)線程的執(zhí)行,不建議直接使用Thread.stop()方法,安全的取消線程即run方法執(zhí)行結(jié)束。只要控制住循環(huán),就可以讓run方法結(jié)束,也就是線程結(jié)束
while ((len = is.read(buffer)) != -1) { //是否繼續(xù)下載 if (!isGoOn) break; }
斷點(diǎn)續(xù)傳即其實(shí)和重新下載是一樣的,不過(guò)文件的大小和每一個(gè)線程下載時(shí)的起始位置和結(jié)束位置都不是重新計(jì)算的。而是上次取消下載時(shí),每一個(gè)線程保存的當(dāng)前位置和結(jié)束位置,讓每一個(gè)線程接著上次的地方繼續(xù)下載即可
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); //獲取上次取消下載的進(jìn)度,若沒(méi)有則返回0 currLength = sp.getInt(CURR_LENGTH, 0); for (int i = 0; i < threadCount; i++) { //開(kāi)始位置,獲取上次取消下載的進(jìn)度,默認(rèn)返回i*blockLength,即第i個(gè)線程開(kāi)始下載的位置 int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength); //結(jié)束位置,-1是為了防止上一個(gè)線程和下一個(gè)線程重復(fù)下載銜接處數(shù)據(jù) int endPosition = (i + 1) * blockLength - 1; //將最后一個(gè)線程結(jié)束位置擴(kuò)大,防止文件下載不完全,大了不影響,小了文件失效 if ((i + 1) == threadCount) endPosition = endPosition * 2; mThreads[i] = new DownThread(i + 1, startPosition, endPosition); mThreads[i].start(); }
網(wǎng)絡(luò)獲取和讀寫(xiě)SD卡都需要添加相應(yīng)權(quán)限
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
下面貼上全部的代碼,里面有詳細(xì)的注釋DownLoadFile.Java
import android.content.Context; import android.content.SharedPreferences; import android.os.Handler; import android.os.Message; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; /** * Created by tianzhao on 2017/2/21 09:25. * 多線程下載文件 */ public class DownLoadFile { private static final String SP_NAME = "download_file"; private static final String CURR_LENGTH = "curr_length"; private static final int DEFAULT_THREAD_COUNT = 4;//默認(rèn)下載線程數(shù) //以下為線程狀態(tài) private static final String DOWNLOAD_INIT = "1"; private static final String DOWNLOAD_ING = "2"; private static final String DOWNLOAD_PAUSE = "3"; private Context mContext; private String loadUrl;//網(wǎng)絡(luò)獲取的url private String filePath;//下載到本地的path private int threadCount = DEFAULT_THREAD_COUNT;//下載線程數(shù) private int fileLength;//文件總大小 //使用volatile防止多線程不安全 private volatile int currLength;//當(dāng)前總共下載的大小 private volatile int runningThreadCount;//正在運(yùn)行的線程數(shù) private Thread[] mThreads; private String stateDownload = DOWNLOAD_INIT;//當(dāng)前線程狀態(tài) private DownLoadListener mDownLoadListener; public void setOnDownLoadListener(DownLoadListener mDownLoadListener) { this.mDownLoadListener = mDownLoadListener; } interface DownLoadListener { //返回當(dāng)前下載進(jìn)度的百分比 void getProgress(int progress); void onComplete(); void onFailure(); } public DownLoadFile(Context mContext, String loadUrl, String filePath) { this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, null); } public DownLoadFile(Context mContext, String loadUrl, String filePath, DownLoadListener mDownLoadListener) { this(mContext, loadUrl, filePath, DEFAULT_THREAD_COUNT, mDownLoadListener); } public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount) { this(mContext, loadUrl, filePath, threadCount, null); } public DownLoadFile(Context mContext, String loadUrl, String filePath, int threadCount, DownLoadListener mDownLoadListener) { this.mContext = mContext; this.loadUrl = loadUrl; this.filePath = filePath; this.threadCount = threadCount; runningThreadCount = 0; this.mDownLoadListener = mDownLoadListener; } /** * 開(kāi)始下載 */ protected void downLoad() { //在線程中運(yùn)行,防止anr new Thread(new Runnable() { @Override public void run() { try { //初始化數(shù)據(jù) if (mThreads == null) mThreads = new Thread[threadCount]; //建立連接請(qǐng)求 URL url = new URL(loadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.setRequestMethod("GET"); int code = conn.getResponseCode();//獲取返回碼 if (code == 200) {//請(qǐng)求成功,根據(jù)文件大小開(kāi)始分多線程下載 fileLength = conn.getContentLength(); //根據(jù)文件大小,先創(chuàng)建一個(gè)空文件 //“r“——以只讀方式打開(kāi)。調(diào)用結(jié)果對(duì)象的任何 write 方法都將導(dǎo)致拋出 IOException。 //“rw“——打開(kāi)以便讀取和寫(xiě)入。如果該文件尚不存在,則嘗試創(chuàng)建該文件。 //“rws“—— 打開(kāi)以便讀取和寫(xiě)入,對(duì)于 “rw”,還要求對(duì)文件的內(nèi)容或元數(shù)據(jù)的每個(gè)更新都同步寫(xiě)入到底層存儲(chǔ)設(shè)備。 //“rwd“——打開(kāi)以便讀取和寫(xiě)入,對(duì)于 “rw”,還要求對(duì)文件內(nèi)容的每個(gè)更新都同步寫(xiě)入到底層存儲(chǔ)設(shè)備。 RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.setLength(fileLength); raf.close(); //計(jì)算各個(gè)線程下載的數(shù)據(jù)段 int blockLength = fileLength / threadCount; SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); //獲取上次取消下載的進(jìn)度,若沒(méi)有則返回0 currLength = sp.getInt(CURR_LENGTH, 0); for (int i = 0; i < threadCount; i++) { //開(kāi)始位置,獲取上次取消下載的進(jìn)度,默認(rèn)返回i*blockLength,即第i個(gè)線程開(kāi)始下載的位置 int startPosition = sp.getInt(SP_NAME + (i + 1), i * blockLength); //結(jié)束位置,-1是為了防止上一個(gè)線程和下一個(gè)線程重復(fù)下載銜接處數(shù)據(jù) int endPosition = (i + 1) * blockLength - 1; //將最后一個(gè)線程結(jié)束位置擴(kuò)大,防止文件下載不完全,大了不影響,小了文件失效 if ((i + 1) == threadCount) endPosition = endPosition * 2; mThreads[i] = new DownThread(i + 1, startPosition, endPosition); mThreads[i].start(); } } else { handler.sendEmptyMessage(FAILURE); } } catch (Exception e) { e.printStackTrace(); handler.sendEmptyMessage(FAILURE); } } }).start(); } /** * 取消下載 */ protected void cancel() { if (mThreads != null) { //若線程處于等待狀態(tài),則while循環(huán)處于阻塞狀態(tài),無(wú)法跳出循環(huán),必須先喚醒線程,才能執(zhí)行取消任務(wù) if (stateDownload.equals(DOWNLOAD_PAUSE)) onStart(); for (Thread dt : mThreads) { ((DownThread) dt).cancel(); } } } /** * 暫停下載 */ protected void onPause() { if (mThreads != null) stateDownload = DOWNLOAD_PAUSE; } /** * 繼續(xù)下載 */ protected void onStart() { if (mThreads != null) synchronized (DOWNLOAD_PAUSE) { stateDownload = DOWNLOAD_ING; DOWNLOAD_PAUSE.notifyAll(); } } protected void onDestroy() { if (mThreads != null) mThreads = null; } private class DownThread extends Thread { private boolean isGoOn = true;//是否繼續(xù)下載 private int threadId; private int startPosition;//開(kāi)始下載點(diǎn) private int endPosition;//結(jié)束下載點(diǎn) private int currPosition;//當(dāng)前線程的下載進(jìn)度 private DownThread(int threadId, int startPosition, int endPosition) { this.threadId = threadId; this.startPosition = startPosition; currPosition = startPosition; this.endPosition = endPosition; runningThreadCount++; } @Override public void run() { SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); try { URL url = new URL(loadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); conn.setConnectTimeout(5000); //若請(qǐng)求頭加上Range這個(gè)參數(shù),則返回狀態(tài)碼為206,而不是200 if (conn.getResponseCode() == 206) { InputStream is = conn.getInputStream(); RandomAccessFile raf = new RandomAccessFile(filePath, "rwd"); raf.seek(startPosition);//跳到指定位置開(kāi)始寫(xiě)數(shù)據(jù) int len; byte[] buffer = new byte[1024]; while ((len = is.read(buffer)) != -1) { //是否繼續(xù)下載 if (!isGoOn) break; //回調(diào)當(dāng)前進(jìn)度 if (mDownLoadListener != null) { currLength += len; int progress = (int) ((float) currLength / (float) fileLength * 100); handler.sendEmptyMessage(progress); } raf.write(buffer, 0, len); //寫(xiě)完后將當(dāng)前指針后移,為取消下載時(shí)保存當(dāng)前進(jìn)度做準(zhǔn)備 currPosition += len; synchronized (DOWNLOAD_PAUSE) { if (stateDownload.equals(DOWNLOAD_PAUSE)) { DOWNLOAD_PAUSE.wait(); } } } is.close(); raf.close(); //線程計(jì)數(shù)器-1 runningThreadCount--; //若取消下載,則直接返回 if (!isGoOn) { //此處采用SharedPreferences保存每個(gè)線程的當(dāng)前進(jìn)度,和三個(gè)線程的總下載進(jìn)度 if (currPosition < endPosition) { sp.edit().putInt(SP_NAME + threadId, currPosition).apply(); sp.edit().putInt(CURR_LENGTH, currLength).apply(); } return; } if (runningThreadCount == 0) { sp.edit().clear().apply(); handler.sendEmptyMessage(SUCCESS); handler.sendEmptyMessage(100); mThreads = null; } } else { sp.edit().clear().apply(); handler.sendEmptyMessage(FAILURE); } } catch (Exception e) { sp.edit().clear().apply(); e.printStackTrace(); handler.sendEmptyMessage(FAILURE); } } public void cancel() { isGoOn = false; } } private final int SUCCESS = 0x00000101; private final int FAILURE = 0x00000102; private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (mDownLoadListener != null) { if (msg.what == SUCCESS) { mDownLoadListener.onComplete(); } else if (msg.what == FAILURE) { mDownLoadListener.onFailure(); } else { mDownLoadListener.getProgress(msg.what); } } } }; }
在MainActivity中的使用
import android.os.Bundle; import android.os.Environment; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends AppCompatActivity { DownLoadFile downLoadFile; private String loadUrl = "http://gdown.baidu.com/data/wisegame/d2fbbc8e64990454/wangyiyunyinle_87.apk"; private String filePath = Environment.getExternalStorageDirectory()+"/"+"網(wǎng)易云音樂(lè).apk"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView tvprogress = (TextView) findViewById(R.id.tv_progress); downLoadFile = new DownLoadFile(this,loadUrl, filePath, 3); downLoadFile.setOnDownLoadListener(new DownLoadFile.DownLoadListener() { @Override public void getProgress(int progress) { tvprogress.setText("當(dāng)前進(jìn)度 :"+progress+" %"); } @Override public void onComplete() { Toast.makeText(MainActivity.this,"下載完成",Toast.LENGTH_SHORT).show(); } @Override public void onFailure() { Toast.makeText(MainActivity.this,"下載失敗",Toast.LENGTH_SHORT).show(); } }); findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.downLoad(); } }); findViewById(R.id.bt_pause).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.onPause(); } }); findViewById(R.id.bt_start).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.onStart(); } }); findViewById(R.id.bt_cancel).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downLoadFile.cancel(); } }); } @Override protected void onDestroy() { downLoadFile.onDestroy(); super.onDestroy(); } }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android FTP 多線程斷點(diǎn)續(xù)傳下載\上傳的實(shí)例
- Android多線程+單線程+斷點(diǎn)續(xù)傳+進(jìn)度條顯示下載功能
- Android多線程斷點(diǎn)續(xù)傳下載功能實(shí)現(xiàn)代碼
- Android多線程斷點(diǎn)續(xù)傳下載示例詳解
- Android 使用AsyncTask實(shí)現(xiàn)多任務(wù)多線程斷點(diǎn)續(xù)傳下載
- Android實(shí)現(xiàn)網(wǎng)絡(luò)多線程斷點(diǎn)續(xù)傳下載實(shí)例
- Android編程開(kāi)發(fā)實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳下載器實(shí)例
- PC版與Android手機(jī)版帶斷點(diǎn)續(xù)傳的多線程下載
- Android 使用AsyncTask實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
- android原生實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳功能
相關(guān)文章
Android學(xué)習(xí)筆記(二)App工程文件分析
之前寫(xiě)過(guò)一篇關(guān)于安卓環(huán)境配置以及第一個(gè)app的制作過(guò)程,下面我們來(lái)進(jìn)一步,分析下APP工程文件2014-07-07Android常用布局(FrameLayout、LinearLayout、RelativeLayout)詳解
這篇文章主要為大家詳細(xì)介紹了Android常用布局FrameLayout、LinearLayout、RelativeLayout,感興趣的小伙伴們可以參考一下2016-06-06為Android Studio編寫(xiě)自定義Gradle插件的教程
這篇文章主要介紹了為Android Studio編寫(xiě)自定義Gradle插件的教程,Android Studio現(xiàn)在基本上已經(jīng)成為了安卓開(kāi)發(fā)的標(biāo)配IDE,友可以參考下2016-02-02Presenting?Streams?in?Flutter小技巧
這篇文章主要為大家介紹了Presenting?Streams?in?Flutter小技巧示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Android實(shí)現(xiàn)3D層疊式卡片圖片展示
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)3D層疊式卡片圖片展示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10Android自定義控件實(shí)現(xiàn)手勢(shì)密碼
這篇文章主要介紹了Android自定義控件實(shí)現(xiàn)手勢(shì)密碼的相關(guān)資料,實(shí)現(xiàn)手勢(shì)解鎖功能,感興趣的小伙伴們可以參考一下2016-07-07Android實(shí)現(xiàn)簡(jiǎn)易版彈鋼琴效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)簡(jiǎn)易版彈鋼琴效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05Android通過(guò)HTTP協(xié)議實(shí)現(xiàn)斷點(diǎn)續(xù)傳下載實(shí)例
本篇文章主要介紹了Android通過(guò)HTTP協(xié)議實(shí)現(xiàn)斷點(diǎn)續(xù)傳下載實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04Android?Java?try?catch?失效問(wèn)題及解決
這篇文章主要介紹了Android?Java?try?catch?失效問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11