編輯:關於Android編程
線程池簡單點就是任務隊列+線程組成的。接下來我們來簡單的了解下ThreadPoolExecutor的源碼。
先看ThreadPoolExecutor的簡單類圖,對ThreadPoolExecutor整體來個簡單的認識。
為了分析ThreadPoolExecutZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcs7Sw8e1w8/Cs7a147bTwdC6zbbTwdDA78PmtcTIzs7x1eK2q873oaM8L3A+DQo8cD6zo7z7yP3W1kJsb2NraW5nUXVldWXX6Mj7ttPB0FN5bmNocm9ub3VzUXVldWWjrExpbmtlZEJsb2NraW5nUXVldWWjrEFycmF5QmxvY2tpbmdRdWV1ZbWxyLu7udPQxuTL+7XEo6y88rWlwODNvCjWu7utwctTeW5jaHJvbm91c1F1ZXVltcS88rWlwODNvCk8L3A+DQo8cD48aW1nIGFsdD0="這裡寫圖片描述" src="/uploadfile/Collfiles/20160330/20160330091329165.png" title="\" />
隊列裡面的任務FutureTask 簡單類圖
按前面所說對於ThreadPoolExecutor我們先關注兩個東西。
BlockingQueueu 隊列他決定了任務的調度方式,我們主要關注BlockingQueue的offer, poll,take三個方法offer往隊列裡面添加任務如果隊列已經滿了話返回false,poll在規定的時間內從隊列裡面取出任務如果隊列是空的就返回null, take也是從隊列裡面取出任務如果隊列是空的則阻塞(保證線程池核心線程一直存在的時候有妙用)
SynchronousQueue:這種queue你直接offer()是沒有用的,必須要有另外一個線程還在poll()的時候才能offer成功。要兩個地方配合使用(對於SynchronousQueue來說這個元素只是走了一個過場罷了一下子就取出來了SynchronousQueue的長度一直是0)。SynchronousQueue在什麼地方用呢,比如Executors.newCachedThreadPool() 這一類線程池的隊列就是用的SynchronousQueue,本來這類線程池的初衷是不用隊列的submit一個任務就開一個線程,任務運行完線程結束。使用SynchronousQueue是為了不用每次submit一個任務的時候都去另開線程,如果submit的時候正好有一個線程執行完了一個任務在poll的時候還是由這個線程來執行這個任務。
ArrayBlockingQueue:基於數組的queue的,先進先出。要設置queue的大小。
LinkedBlockingQueue:基於鏈表的queue,先進先出,可以設置也可以不設置queue的大小,不設置就是默認的大小。
BlockingQueue 隊列裡面放得是FutureTask,從隊列裡面把FutureTask任務拿出來之後調用的是FutureTask的run方法。run方法裡面會調用FutureTask裡面Callable的call方法,call方法調用完之後保存住了call的返回值。這樣FutureTask就可以通過get方法得到這個返回值。
先說下ExecutorService(ThreadPoolExecutor實現了這個接口,從ThreadPoolExecutor的類圖可以看出)接口裡面一些方法具體作用。
// 不讓再submit新的任務了,但是之前提交的還是會繼續執行完的。
void shutdown();
// 不讓再submit新的任務了,並且嘗試去停止線程池裡面所有的任務,不管是正在執行的還是沒有執行的,並且返回沒有執行的任務列表
List shutdownNow();
// 線程池是否shut down了
boolean isShutdown();
// 線程池是否終止了
boolean isTerminated();
// 等待線程池終止,如果在timeout時間之內終止了就返回ture,否則返回false。一般配合shutdown函數使用
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
// 提交任務
Future submit(Callable task);
Future submit(Runnable task, T result);
Future submit(Runnable task);
// 執行tasks裡面所有的callable,返回所有的情況結果
List> invokeAll(Collection> tasks) throws InterruptedException;
// 在timeout時間內執行tasks裡面所有的callable,返回所有的情況結果(包括沒執行的也會返回)
List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException;
// 執行tasks裡面所有的callable,當有一個處理完了就結束
T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException;
// 在timeout時間內執行tasks裡面所有的callable,當有一個處理完了就結束
T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
下面正式開始ThreadPoolExecutor的源碼分析(只是分析了部分函數)按照線程池的使用流程來看ThreadPoolExecutor的源碼。先構造函數,在submit方法, 然後在shutdown方法。
我們得先知道線程池總共有5中狀態 如下所示
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
RUNNING:表示線程池能夠接受任務,並且可以運行隊列中的任務。
SHUTDOWN:表示不接受新的任務,但是之前隊列裡面的任務還是會被調用(調用了shutdown()之後的狀態)
STOP:表示不接受新的任務,不會執行隊列中的任務,並且嘗試去中斷正在運行的任務(調用了shutdownNow()的狀態)
TIDYING:表示所有任務都已經終止,workCount值為0(workCount可以理解成線程的個數)轉到TIDYING狀態的線程即將要執行terminated()鉤子方法。
TERMINATED:表示terminated()方法執行結束。
5中狀態的轉換有以下幾種方式。
RUNNING -> SHUTDOWN:調用了shutdown方法,或者線程池實現了finalize方法,在裡面調用了shutdown方法。
(RUNNING or SHUTDOWN) -> STOP:調用了shutdownNow方法
SHUTDOWN -> TIDYING:當隊列和線程池均為空的時候
STOP -> TIDYING:當線程池為空的時候
TIDYING -> TERMINATED:terminated()鉤子方法調用完畢
ThreadPoolExecutor4個構造函數不管是從哪個構造函數進來的最後走的都是最後一個
...
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
...
corePoolSize:核心線程個數。
maximumPoolSize:最大線程個數 最大線程個數要大於核心線程個數(maximumPoolSize>corePoolSize)
workQueue: 線程池任務隊列(線程池關鍵地方,有時候注重任務出隊的順序或者任務有優先級都要靠他來實現)。
keepAliveTime:核心線程之外的線程如果達到了這個空閒時間線程自動關閉(當然也可以作用於核心線程通過allowsCoreThreadTimeOut()函數)。
unit:keepAliveTime時間的單位。
threadFactory:線程工程用來創建線程的,把這個暴露給我們是為了讓我們可以控制創建線程的一些行為,比如設置線程的優先級,名字,debug等等。
handler:對於reject的任務該怎麼處理就是靠這個來完成的(當workQueue滿了並且達到了最大線程的個數的時候會拒絕加進來的任務,或者調用了shutdown函數之後再加入任務也是會reject的)。
submit函數(AbstractExecutorService類中)
...
public Future submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public Future submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
public Future submit(Callable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
...
不管調用的是哪個submit方法都是先構造出一個RunnableFuture(FutureTask) 然後調用execute方法。不管你submit的時候傳入的是Runnable還是Callable最後RunnableFuture(FutureTask)裡面都會生成Callable對象。任務調用的時候調用RunnableFuture(FutureTask)的run方法,run方法調用Callable對象的call方法。接著看execute方法。
...
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
...
注釋中我們也看到了總共分為三步。1. 如果當前線程池中的線程個數小於核心線程數則調用addWorker方法第一個參數是任務,第二個參數表示是不是核心線程(addWorker等下再看)。2. 如果當前的線程池是RUNNING狀態則任務加入線程池,加入到隊列之後再檢測一次狀態如果不是RUNNING狀態,把這個任務從任務中移除 reject這個任務(reject等下再看),else如果線程池中線程為0表示沒有指定核心線程個數,還是addWorker 注意addWorker的參數。3. 可能是隊列滿了用核心線程之外的線程去處理任務 還是addWorker。
這裡我們兩個函數我們沒有分析reject 和 addWorker函數,先看簡單的reject函數。reject簡單點
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
handler 是RejectedExecutionHandler 對於reject的任務可以做不同的處理,拋出異常或者不做任何處理。隨你如果你想自己處理,這個應該還好說。
addWorker通過這個函數去開線程,第一個參數firstTask如果不為空則開的線程直接執行這個Runnable,如果為空則開的線程去隊列裡面拿任務,第二個參數core表示准備開的線程是不是核心線程判斷線程個數用的。
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
8~12行 if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null &&! workQueue.isEmpty())) 轉換成if (rs >= SHUTDOWN && ((rs != SHUTDOWN || firstTask != null || workQueue.isEmpty()))) 前半部分狀態是這四種才能進入SHUTDOWN,STOP,TIDYING,TERMINATED 這四種狀態才會進入(這裡注意前面說的線程池各種狀態的含義哦),後半部分第一個條件rs != SHUTDOWN 又給我們去掉了一種狀態STOP,TIDYING,TERMINATED這三種狀態時既不能去執行新加的任務也不能執行隊列裡面的任務直接return false。第二個條件 狀態為SHUTDOWN且firstTask != null意思是說在SHUTDOWN狀態還想添加新的任務return false(SHUTDOWN狀態是不能添加新任務吧),第三個添加workQueue.isEmpty()表示SHUTDOWN且 firstTask == null 且隊列為空,表示SHUTDOWN狀態去隊列裡面取任務執行,但是這個時候隊列裡面沒有任務了return false。 這裡可能說的有點亂總結下這幾行代碼。三種情況
1)線程池狀態是STOP,TIDYING,TERMINATED 不能再去增開線程,不管你開的這個線程是去取隊列的任務還是直接執行你submit的任務都是不可以的。
2)線程池狀態是SHUTDOWN的你又想去開線程執行你submit的任務 對不起reject
3)線程池狀態是SHUTDOWN 隊列裡面沒有任務,這個時候你又想去隊列裡面取出任務執行。對不起不行。隊列為空你肯定取不到這個任務。
16~18行,要開的線程是核心線程 線程池個數肯定要小於核心線程個數吧,不是核心線程,線程個數肯定要小於最大線程個數吧。
19行,線程池裡面的線程個數加一。
32行,出現了Worker worker裡面有一個Thread 這個我們等下再看。先往下看
47行,放到workers裡面去,workers 裡面放的是所有的線程。先這麼理解吧。
57行,t.start() Worker裡面的線程池跑起來了。
接下來就該到了Worker 類了。
Worker 類實現了Runnable方法。注意Worker的構造函數裡面this.thread = getThreadFactory().newThread(this); newThread的參數是this是Worker本身。所以前面addWorker函數裡面t.start() Worker裡面的線程跑起來了。直接調用的是Woker類裡面自身的run方法。那就直接看Worker類裡面的run方法。
public void run() {
runWorker(this);
}
恩 runWorker(this),又是把自身給傳入進去了,沒什麼說的進去看吧,這裡面干的事情就是沒執行完一個任務又去隊列裡面取下一個任務執行,如果沒取到線程結束線程個數減掉1。看具體的實現。ThreadPoolExecutor 類裡面runWorker函數
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
第8行 while (task != null || (task = getTask()) != null) 先判斷是不是去執行submit進來的任務如果不是則是去隊列裡面取任務執行(while完之後task又會賦值null讓他去隊列裡面取任務執行)。getTask函數我們等下再看,我們知道了他是去隊列裡面取任務的。
第20行 和第31行 beforeExecute(wt, task); afterExecute(task, thrown);給我們上層重寫用的每個任務的執行前和執行後都會調用者兩個方法。
第23行 task.run(); 真正每個任務要做的邏輯在這個裡面。而且我們前面也說過task是FutureTask,調用FutureTask裡面的run方法會調用FutureTask裡面Callable的call方法,call方法調用完之後保存住了call的返回值。FutureTask 可以通過get方法得到這個返回值。
第34行 看task = null;了吧。
到這裡就差getTask() 方法了。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
9~12行,兩種情況第一種 線程池狀態是STOP,TIDYING,TERMINATED不能再去隊列裡面拿任務執行了,第二種線程池狀態是SHUTDOWN 隊列裡面又沒有任務不能再提供任務這個線程了。workcount減掉1
17行,判斷這個線程有沒有timeout,如果沒有timeout的線程是不會自動停掉的會一直存在(因為有的時候想一直保持核心線程的個數如果沒有特殊的設置)。allowCoreThreadTimeOut如果設置了則所有的線程都有timeout包括核心線程,wc > corePoolSize 為了讓核心線程之外的線程能夠停掉。
27~29行 從隊列中去取任務 poll在規定的時間內從隊列裡面取出任務如果隊列是空的就返回null(表示沒取到線程也就結束了), take也是從隊列裡面取出任務如果隊列是空的則阻塞保證線程池裡面的核心線程數量的線程一直存在。隊列裡面poll和take方法的妙用。
submit函數的調用過程就說完了,下面是shutdown函數
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
5行,使得線程可以shutdown(interrupt)
6行,切換線程池的狀態
7行,interruptIdleWorkers方法中斷空閒的線程。接下來分析
8行,ouShutdown()空方法給我們重寫用的。
12行,嘗試去terminate線程池,接下來分析。
這裡我們要分析interruptIdleWorkers和tryTerminate方法
先interruptIdleWorkers 這個方法是去中斷空閒線程。
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
在進interruptIdleWorkers方法。
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
第6行,拿到worker對應的Thread。
第7行,如果當前線程沒有被中斷且可以拿到worker的鎖,則中斷worker對應的線程。如果我們拿到worker的鎖說明worker對應的線程是空閒的,為什麼這麼說呢,看worker的run方法。lock加鎖是在while裡面的。
tryTerminate方法 當線程池的狀態為SHUTDOWN且任務隊列為空,需要將池的狀態轉變為TERMINATED;當池的狀態為STOP且池中的當前活動線程數為0,要將池的狀態轉換成TERMINATED。
final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
4~7行,表示有三種情況是不用設置線程池的狀態到TERMINATED,第一種當前線程是RUNNING的狀態,第二種當前狀態為TIDYING或TERMINATED,池中的活動線程數已經是0,第三種當前狀態是SHUTDWON,但隊列中還有任務也不用做處理因為這種情況下任務還是要處理掉的。
剩下的情況就兩種吧才會繼續往下走吧 一種當前的狀態是STOP不管隊列裡面有沒有任務,還有一種是當前狀態是SHUTDOWN狀態且隊列裡面沒有任務了。這兩種情況會想辦法切換到TERMINATED狀態去。
8行,線程池中的線程數不為0,只是嘗試去中斷一個空閒線程,為什麼這麼干還沒理解。
16行,把線程池狀態切換到TIDYING。
18行,terminated();給我們重寫用的。
20行,把線程池狀態切換到TERMINATED。
總結。
線程池 先看隊列是什麼形式的隊列,是先進先出的,還是在入隊的時候會做排序操作。關注隊列的offer,poll,take方法。offer入隊的時候調用,poll當我們對線程設置了timeout的時候會調用poll方法去隊列裡面去任務 如果指定時間內沒取到改線程也就結束了。take阻塞的形式去取任務線程不會退出有的時候用了保證核心線程個數的線程一直存在。 線程池 隊列裡面的任務FutureTask 裡面實現了run方法,run方法裡面又會調用FutureTask裡面Callable對象的call方法,所以每個任務在入隊的時候不管你submit方法傳入的是Runnable 還是 Callable 最後都會同意成Callable。 只有當達到了核心線程數,並且隊列滿了的時候才會去啟動其他的線程。如果圖片資源是靜態的,當我們要在View上顯示圖片時,只需要簡單的將圖片賦值給ImageView就可以了,但如果需要浏覽網絡上的圖片時該如何做呢?有可能圖片很大,有可能網
DRM In this document Overview Android DRM FrameworkWidevine DR
Intent隱式通訊Intent對象可以向操作系統描述我們需要處理的任務。使用顯式intent,我們需明確地告訴操作系統要啟動的activity類名。下面是之前創建過的顯
本文實例講述了Android TreeView效果實現方法。分享給大家供大家參考,具體如下:應該說很多的操作系統上面都提供了TreeView空間,實現樹形結構,這個樹形結