Ⅰ android圖形系統(八)-app與SurfaceFlinger共享UI元數據過程
Android應用程序與SurfaceFlinger服務是運行在不同的進程中的,因此,它們採用Binder進程間通信機制來進行通信。
但是我們知道一個Android應用程序可能會有很多個窗口,而每一個窗口都有自己的UI元數據,因此,Android應用程序需要傳遞給SurfaceFlinger服務的UI元數據是相當可觀的。在這種情況下,通過Binder來在Android應用程序與SurfaceFlinger服務之間傳遞UI元數據是不合適的,因此這里選擇了Android系統的匿名共享內存的方案。在每一個Android應用程序與SurfaceFlinger服務之間的連接上加上一塊用來傳遞UI元數據的匿名共享內存。而這塊區域被包裝為SharedClient。
在每一個SharedClient裡面,有至多31個SharedBufferStack,那什麼又是SharedBufferStack?
SharedBufferStack就是共享緩沖區堆棧,每一個SharedBufferStack與一個Surface一一對應,每一個Surface又對應一個窗口,那就是一個應用程序內部最多可創建31個窗口。SharedBufferStack 內部包含N個緩沖buffer, 開篇介紹的雙緩沖(front buffer , back buffer) ,三緩沖(front buffer , back buffer, tripple buffer),有了它SurfaceFlinger服務就可以使用N個緩沖區技術來繪制UI了。
下面我們再來了解下SharedBufferStack的結構:
SharedBufferStack中分為空閑buffer和已使用的buffer。其中SharedBufferStack中的每一個已經使用了的緩沖區都對應有一個GraphicBuffer,用來描述真正的UI數據。
客戶端一次申請GraphicBuffer且將UI元數據寫入GraphicBuffer的流程:
當Android應用程序需要更新一個Surface的時候,它就會找到與它所對應的SharedBufferStack,並且從它的空閑緩沖區列表的尾部取出一個空閑的Buffer。我們假設這個取出來的空閑Buffer的編號為index。接下來Android應用程序就請求SurfaceFlinger服務為這個編號為index的Buffer分配一個圖形緩沖區GraphicBuffer。SurfaceFlinger服務分配好圖形緩沖區GraphicBuffer之後,會將它的編號設置為index,然後再將這個圖形緩沖區GraphicBuffer返回給Android應用程序訪問。Android應用程序得到了SurfaceFlinger服務返回的圖形緩沖區GraphicBuffer之後,就在裡面寫入UI數據。寫完之後,就將與它所對應的緩沖區,即編號為index的Buffer,插入到對應的SharedBufferStack的已經使用了的緩沖區列表的頭部去。這一步完成了之後,Android應用程序就通知SurfaceFlinger服務去繪制那些保存在已經使用了的緩沖區所描述的圖形緩沖區GraphicBuffer了。
那麼我們也知道一個繪圖表面,在SurfaceFlinger服務和Android應用程序中分別對應Layer對象和Surface對象,其中這兩個對象在內部分別使用一個SharedBufferServer對象和一個SharedBufferClient對象來操作這個繪圖表面的UI元數據緩沖堆棧。操作過程如下:
在Android應用程序這一側,當它需要渲染一個Surface時,它就會首先找到對應的SharedBufferClient對象,然後再調用它的成員函數dequeue來請求分配一個UI元數據緩沖區。有了這個UI元數據緩沖區之後,Android應用程序再調用這個SharedBufferClient對象的成員函數setDirtyRegion、setCrop和setTransform來設置對應的Surface的裁剪區域、紋理坐標以及旋轉方向。此外,Android應用程序還會請求SurfaceFlinger服務為這個Surface分配一個圖形緩沖區,以便可以往這個圖形緩沖區寫入實際的UI數據。最後,Android應用程序就可以調用這個SharedBufferClient對象的成員函數queue把前面已經准備好了的UI元數據緩沖區加入到它所描述的一個UI元數據緩沖區堆棧的待渲染隊列中,以便SurfaceFlinger服務可以在合適的時候對它進行渲染。當SurfaceFlinger服務需要渲染一個Surface的時候,它就會找到對應的一個SharedBufferServer對象,然後調用它的成員函數getQueueCount來檢查它所描述的一個UI元數據緩沖區堆棧的待渲染隊列的大小。如果這個大小大於0,那麼SurfaceFlinger服務就會繼續調用它的成員函數retireAndLock來取出隊列中的第一個UI元數據緩沖區,以及調用它的成員函數getDirtyRegion、getCrop和getTransform來獲得要渲染的Surface的裁剪區域、紋理坐標和旋轉方向。最後,SurfaceFlinger服務就可以結合這些信息來將保存這個Surface的圖形緩沖區中的UI數據渲染在顯示屏中。
另外想深入了解BufferQueue的生產者消費者模型,詳細可以閱讀下如下這篇博文,感覺還不錯: https://blog.csdn.net/stn_lcd/article/details/73801313
參考:
https://blog.csdn.net/Luoshengyang/article/details/7867340
Ⅱ Android消息隊列淺析
當面試官問到你消息對列的時候,恭喜你,已經跨過初級,在試探你的中級水平了。
Android的消息循環是參考Windows的消息循環機制來實現的。
消息隊列4件套 Message、MessageQueue、Looper、Handler
1、Message 是消息對列的消息實體類,因為消息隊列中會存放最多10個Message對象。常用屬性 what,是消息體的Tag,用來區分是那個一消息體。
2、 MessageQueue 先進先出」的原則存放消息,將Message對象以鏈表的方式串聯起來。
3、Looper 是MessageQueue的管理者,主線程中是一對一的關系。子線程需要用到消息對列的話就需要經典二人組 。先調用 Looper.prepare()方法,然後再調用Looper.loop();
4、Handler 是封裝和處理Message對象的。
通過源碼可知消息走向如下
handler.sendMessage()-->handler.sendMessageDelayed()-->handler.sendMessageAtTime()-->msg.target = this;queue.enqueueMessage==>把msg添加到消息隊列中
Ⅲ Android中的線程池
線程池的好處
1、重用線程池中的線程,避免線程的創建與銷毀帶來的性能開銷
2、能有效控制線程池的最大並發數,避免大量線程因搶占資源而導致的阻塞
3、能對線程進行簡單的管理,提供定時或者指定間隔時間、循環執行等操作
線程池的概率來自於java的Executor介面,實現類是ThreadPoolExecutor, 它提供一系列的參數來配置線程池,以此構建不同的線程池。Android的線程池分4類,都是通過Executors所提供的工廠方法來得到。
ThreadPoolExecutor有四個構造函數,下面這個是最常用的
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnnable> workQueue, ThreadFactory threadFactory)
corePoolSize
線程池中的核心線程數,默認情況下核心線程會在線程池中一直存活,即使他們處於閑置狀態。如果設置ThreadPoolExecutor 中的allowCoreThreadTimeOut = true, 核心線程在等待新任務到來時有超時機制,時間超過keepAliveTime所指定的時間後,核心線程會終止。
maximumPoolSize
最大線程數
keepAliveTime
非核心線程閑置的超時時間,超過這個時間,非核心線程會被回收。核心線程則要看allowCoreThreadTimeOut屬性的值。
unit
時間單位
workQueue
線程池中的工作隊列
threadFactory
線程工廠,為線程池提供創建新線程的功能。
舉個例子,我們常用的okhttp內部也是使用了線程池,它的ThreadPoolExecutor主要是定義在Dispatcher類裡面。 使用的是CachedThreadPool。
executorService = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS, SynchronousQueue(), ThreadFactory("okhttp Dispatcher", false))
1、FixedThreadPool
通過Executors的newFixedThreadPool()創建,這是一個線程數量固定的線程池,裡面所有的線程都是核心線程。
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
}
2、CachedThreadPool
通過Executors的newCacheThreadPool()創建,這是一個線程數量不定的線程池,裡面所有的線程都是非核心線程。最大線程數是無限大,當線程池中的線程都處於活動狀態時,新的task會創建新的線程來處理,否則就使用空閑的線程處理,所有的線程都是60s的超時時間,超時後會自動回收。
public static ExecutorService newFixedThreadPool(){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())
}
3、ScheledThreadPool
通過Executors的newScheledThreadPool()創建, 核心線程固定,非核心線程無限大,當非核心線程空閑時,會立即被回收。適合做定時任務或者固定周期的重復任務。
public static ExecutorService newScheledThreadPool(int corePoolSize){
return new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new DelayedWorkQueue())
}
4、SingleThreadExcecutor
通過Executors的newSingleThreadPool()創建,內部只有一個核心線程。
public static ExecutorService newFixedThreadPool(){
return new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
}
課外知識:LinkedBlockingQueue
LinkedBlockingQueue是由鏈表組成的阻塞隊列,內部head 指向隊列第一個元素,last指向最後一個元素。入隊和出隊都會加鎖阻塞,都是使用了不同的鎖。
DelayedWorkQueue
延時隊列,隊內元素必須是Delayed的實現類。對內元素會按照Delayed時間進行排序,對內元素只有在delayed時間過期了才能出隊。
入隊的時候不阻塞隊列,出隊的時候,如果隊列為空或者隊列里所有元素都等待時間都沒有到期,則該線程進入阻塞狀態。
Ⅳ Android 線程池的封裝
GlobalThreadPools.java:
調用:
線程池
線程池概念來源於Java中的Executor,它是一個介面,真正的實現為ThreadPoolExecutor。ThreadPoolExecutor提供了一系列參數來配置線程池。
優點
1:重用線程池中的線程,線程在執行完任務後不會立刻銷毀,而會等待另外的任務,這樣就不會頻繁地創建、銷毀線程和調用GC。。
2:有效控制線程池的最大並發數,避免大量線程搶占資源出現的問題。
3:對多個線程進行統一地管理,可提供定時執行及指定間隔循環執行的功能。
ThreadPoolExecutor 有多個重載方法,但最終都調用了這個構造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
參數:
corePoolSize:線程池中核心線程的數量;為了內存優化,在線程池維護了幾個重要的線程,不達到一定條件不開辟其餘線程
maximumPoolSize :線程池中最大線程數量:這個數量是包括核心線程的,當線程池中的正在執行的線程池達到了這個數字,再提交線程如果你不做特殊處理將會拋出異常
keepAliveTime:非核心線程的超時時長;當線程池中的非核心線程閑置時間超過這個值代表的時間後,將會被回收;同時如果調用ThreadPoolExecutor.allowCoreThreadTimeOut(true),那麼核心線程也會符合這個設置
unit:keepAliveTime值的單位,可以是時分秒等
workQueue:存放待執行的線程;你通過execute方法提交線程,但是這些線程還沒達到執行條件,那麼就會保存在這個隊列里
threadFactory:創建線程池的工廠;在這個工廠里,我們可以指定線程的一些信息
handler:線程提交拒絕策略;通常是線程池中的正在執行的線程數量已經達到了最大線程數或線程池關閉,如果不傳,默認是拋出一個RejectedExecutionException,所以最好傳下
推薦使用 Executors 的工廠方法來創建線程池,通過直接或間接的配置 ThreadPoolExecutor 的參數來構建線程池,常用的線程池有如下 4 種,newFixedThreadPool ,newCachedThreadPool,
newScheledThreadPool 和 newSingleThreadExecutor。
ThreadPoolExecutor 執行任務時大致遵循如下流程:
1.如果線程池中的線程數未達到核心線程數,則會立馬啟用一個核心線程去執行。
2.如果線程池中的線程數已經達到核心線程數,且任務隊列workQueue未滿,則將新線程放入workQueue中等待執行。
3.如果線程池中的線程數已經達到核心線程數但未超過線程池規定最大值,且workQueue已滿,則開啟一個非核心線程來執行任務。
4.如果線程池中的線程數已經超過線程池規定最大值,則拒絕執行該任務,採取飽和策略,並拋出RejectedExecutionException異常。
線程池大小:(N為CPU數量)
如果是CPU密集型應用,則線程池大小設置為N+1
如果是IO密集型應用,則線程池大小設置為2N+1
I/O密集型
指的是系統的CPU效能相對硬碟/內存的效能要好,大部分的狀況是 CPU 在等 I/O (硬碟/內存) 的讀/寫, CPU Loading 不高。
CPU密集型
指的是系統的 硬碟/內存 效能 相對 CPU 的效能 要好,大部分的狀況是 CPU Loading 100%,CPU 要讀/寫 I/O (硬碟/內存),I/O在很短的時間就可以完成,而 CPU 還有許多運算要處理,CPU Loading 很高。
獲取CPU數量的方法為:
Runtime.getRuntime().availableProcessors();
摘自:
https://blog.csdn.net/qq_30993595/article/details/84324681
Ⅳ android怎麼實現任務隊列
主要就是有一個線程隊列,維護這些任務,這里沒有用到Queue而是用List是考慮到顯示的問題。
Ⅵ Android中的Activity詳解--啟動模式與任務棧
目錄
activity的簡單介紹就不寫了,作為最常用的四大組件之一,肯定都很熟悉其基本用法了。
首先,是都很熟悉的一張圖,即官方介紹的Activity生命周期圖.
情景:打開某個應用的的FirstActivity調用方法如下:
由於之前已經很熟悉了,這里就簡單貼一些圖。
按下返回鍵:
重新打開並按下home鍵:
再重新打開:
在其中打開一個DialogActivity(SecondActivity)
按下返回:
修改SecondAcitvity為普通Activity,依舊是上述操作:
這里強調一下 onSaveInstanceState(Bundle outState) 方法的調用時機:
當Activity有可能被系統殺掉時調用,注意,一定是被系統殺掉,自己調用finish是不行的。
測試如下:FirstActivity啟動SecondActivity:
一個App會包含很多個Activity,多個Activity之間通過intent進行跳轉,那麼原始的Activity就是使用棧這個數據結構來保存的。
Task
A task is a collection of activities that users interact with when performing a certain job. The activities are arranged in a stack (the back stack ), in the order in which each activity is opened.
即若干個Activity的集合的棧表示一個Task。
當App啟動時如果不存在當前App的任務棧就會自動創建一個,默認情況下一個App中的所有Activity都是放在一個Task中的,但是如果指定了特殊的啟動模式,那麼就會出現同一個App的Activity出現在不同的任務棧中的情況,即會有任務棧中包含來自於不同App的Activity。
標准模式,在不指定啟動模式的情況下都是以此種方式啟動的。每次啟動都會創建一個新的Activity實例,覆蓋在原有的Activity上,原有的Activity入棧。
測試如下:在FirstActivity中啟動FirstActivity:
當只有一個FirstActivity時堆棧情況:
此種模式下,Activity在啟動時會進行判斷,如果當前的App的棧頂的Activity即正在活動的Activity就是將要啟動的Activity,那麼就不會創建新的實例,直接使用棧頂的實例。
測試,設置FirstActivity為此啟動模式,多次點擊FirstActivity中的啟動FirstActivity的按鈕查看堆棧情況:
(其實點擊按鈕沒有啟動新Activity的動畫就可以看出並沒有啟動新Activity)
大意就是:
對於使用singleTop啟動或Intent.FLAG_ACTIVITY_SINGLE_TOP啟動的Activity,當該Activity被重復啟動(注意一定是re-launched,第一次啟動時不會調用)時就會調用此方法。
且調用此方法之前會先暫停Activity也就是先調用onPause方法。
而且,即使是在新的調用產生後此方法被調用,但是通過getIntent方法獲取到的依舊是以前的Intent,可以通過setIntent方法設置新的Intent。
方法參數就是新傳遞的Intent.
1.如果是同一個App中啟動某個設置了此模式的Activity的話,如果棧中已經存在該Activity的實例,那麼就會將該Activity上面的Activity清空,並將此實例放在棧頂。
測試:SecondActivity啟動模式設為singleTask,啟動三個Activity:
這個模式就很好記,以此模式啟動的Activity會存放在一個單獨的任務棧中,且只會有一個實例。
測試:SecondActivity啟動模式設為singleInstance
結果:
顯然,啟動了兩次ThirdActivity任務棧中就有兩個實例,而SecondActivity在另外一個任務棧中,且只有一個。
在使用Intent啟動一個Activity時可以設置啟動該Activity的啟動模式:
這個屬性有很多,大致列出幾個:
每個啟動的Activity都在一個新的任務棧中
singleTop
singleTask
用此種方式啟動的Activity,在它啟動了其他Activity後,會自動finish.
官方文檔介紹如下:
這樣看來的話,通俗易懂的講,就是給每一個任務棧起個名,給每個Activity也起個名,在Activity以singleTask模式啟動時,就檢查有沒有跟此Activity的名相同的任務棧,有的話就將其加入其中。沒有的話就按照這個Activity的名創建一個任務棧。
測試:在App1中設置SecondActivity的taskAffinity為「gsq.test」,App2中的ActivityX的taskAffinity也設為「gsq.test」
任務棧信息如下:
結果很顯然了。
測試:在上述基礎上,在ActivityX中進行跳轉到ActivityY,ActivityY不指定啟動模式和taskAffinity。結果如下:
這樣就沒問題了,ActivityY在一個新的任務棧中,名稱為包名。
這時從ActivityY跳轉到SecondActivity,那應該是gsq.test任務棧只有SecondActivity,ActivityX已經沒有了。因為其啟動模式是singleTask,在啟動它時發現已經有一個實例存在,就把它所在的任務棧上面的Activity都清空了並將其置於棧頂。
還有一點需要提一下,在上面,FirstActivity是App1的lunch Activity,但是由於SecondActivity並沒有指定MAIN和LAUNCHER過濾器,故在FirstActivity跳轉到SecondActivity時,按下home鍵,再點開App1,回到的是FirstActivity。
大致就先寫這么多吧,好像有點長,廢話有點多,估計也有錯別字,不要太在意~~~
Ⅶ Android 守護進程的實現方式
在我們進行應用開發時,會遇到上級的各種需求,其中有一條 剛需: 後台保活 ,更有甚者:
我要我們的應用永遠活在用戶的手機後台不被殺死 —— 這都 TM 的扯淡
除了系統級別的應用能持續運行,所有三方程序都有被殺死的那一天!當然 QQ/微信/陌陌 等會好一些,因為他們已經深入設備的 心 ;
我們能做的只是通過各種手段盡量讓我們的程序在後台運行的時間長一些,或者在被幹掉的時候,能夠重新站起來,而且這個也不是每次都有效的,也是不能在所有的設備的上都有效的;要做到後台進程保活,我們需要做到兩方便:
要實現實現上邊所說,通過下邊幾點來實現,首先我們需要了解下進程的優先順序劃分:
Process Importance 記錄在 ActivityManager.java 類中:
了解進程優先順序之後,我們還需要知道一個進程回收機制的東西;這里參考 AngelDevil 在博客園上的一篇文章:
Android 的 Low Memory Killer 基於 Linux 的 OOM 機制,在 Linux 中,內存是以頁面為單位分配的,當申請頁面分配時如果內存不足會通過以下流程選擇bad進程來殺掉從而釋放內存:
在 Low Memory Killer 中通過進程的 oom_adj 與佔用內存的大小決定要殺死的進程, oom_adj 越小越不容易被殺死;
Low Memory Killer Driver 在用戶空間指定了一組內存臨界值及與之一一對應的一組 oom_adj 值,當系統剩餘內存位於內存臨界值中的一個范圍內時,如果一個進程的 oom_adj 值大於或等於這個臨界值對應的 oom_adj 值就會被殺掉。
下邊是表示 Process State (即老版本里的 OOM_ADJ )數值對照表,數值越大,重要性越低,在新版SDK中已經在 android 層去除了小於0的進程狀態
Process State (即老版本的 OOM_ADJ )與 Process Importance 對應關系,這個方法也是在 ActivityManager.java 類中,有了這個關系,就知道可以知道我們的應用處於哪個級別,對於我們後邊優化有個很好地參考
一般情況下,設備端進程被幹掉有一下幾種情況
由以上分析,我們可以可以總結出,如果想提高我們應用後台運行時間,就需要提高當前應用進程優先順序,來減少被殺死的概率
分析了那麼多,現在對Android自身後台進程管理,以及進程的回收也有了一個大致的了解,後邊我們要做的就是想盡一切辦法去提高應用進程優先順序,降低進程被殺的概率;或者是在被殺死後能夠重新啟動後台守護進程
第一種方式就是利用系統漏洞,使用 startForeground() 將當前進程偽裝成前台進程,將進程優先順序提高到最高(這里所說的最高是服務所能達到的最高,即1);
這種方式在 7.x 之前都是很好用的,QQ、微信、IReader、Keep 等好多應用都是用的這種方式實現;因為在7.x 以後的設備上,這種偽裝前台進程的方式也會顯示出來通知欄提醒,這個是取消不掉的,雖然 Google 現在還沒有對這種方式加以限制,不過這個已經能夠被用戶感知到了,這種方式估計也用不了多久了
下邊看下實現方式,這邊這個 VMDaemonService 就是一個守護進程服務,其中在服務的 onStartCommand() 方法中調用 startForeground() 將服務進程設置為前台進程,當運行在 API18 以下的設備是可以直接設置,API18 以上需要實現一個內部的 Service ,這個內部類實現和外部類同樣的操作,然後結束自己;當這個服務啟動後就會創建一個定時器去發送廣播,當我們的核心服務被幹掉後,就由另外的廣播接收器去接收我們守護進程發出的廣播,然後喚醒我們的核心服務;
當我們啟動這個守護進程的時候,就可以使用以下 adb 命令查看當前程序的進程情況(需要 adb shell 進去設備),
為了等下區分進程優先順序,我啟動了一個普通的後台進程,兩外兩個一個是我們啟動的守護進程,一個是當前程序的核心進程,可以看到除了後台進程外,另外兩個進程都帶有 isForeground=true 的屬性:
然後我們可以用下邊的命令查看 ProcessID
有了 ProcessID 之後,我們可以根據這個 ProcessID 獲取到當前進程的優先順序狀態 Process State ,對應 Linux 層的 oom_adj
可以看到當前核心進程的級別為 0 ,因為這個表示當前程序運行在前台 UI 界面,守護進程級別為 1 ,因為我們利用漏洞設置成了前台進程,雖然不可見,但是他的級別也是比較高的,僅次於前台 UI 進程,然後普通後台進程級別為 4 ;當我們退到後台時,可以看到核心進程的級別變為 1 了,這就是因為我們利用 startForeground() 將進程設置成前台進程的原因,這樣就降低了進程被系統回收的概率了;
可以看到這種方式確實能夠提高進程優先順序,但是在一些國產的設備上還是會被殺死的,比我我測試的時候小米點擊清空最近運行的應用進程就別幹掉了;當把應用加入到設備白名單里就不會被殺死了,微信就是這樣,人家直接裝上之後就已經在白名單里了,我們要做的就是在用戶使用中引導他們將我們的程序設置進白名單,將守護進程和白名單結合起來,這樣才能保證我們的應用持續或者
Android系統在5.x以上版本提供了一個 JobSchele 介面,系統會根據自己實現定時去調用改介面傳遞的進程去實現一些操作,而且這個介面在被強制停止後依然能夠正常的啟動;不過在一些國產設備上可能無效,比如小米;
下邊是 JobServcie 的實現:
我們要做的就是在需要的時候調用 JobSchele 的 schele 來啟動任務;剩下的就不需要關心了, JobSchele 會幫我們做好,下邊就是我這邊實現的啟動任務的方法:
在實現 Service 類時,將 onStartCommand() 返回值設置為 START_STICKY ,利用系統機制在 Service 掛掉後自動拉活;不過這種方式只適合比較原生一些的系統,像小米,華為等這些定製化比較高的第三方廠商,他們都已經把這些給限制掉了;
這種方式在以下兩種情況無效:
事事沒有絕對,萬物總有一些漏洞,就算上邊的那些方式不可用了,後邊肯定還會出現其他的方式;我們不能保證我們的應用不死,但我們可以提高存活率;
其實最好的方式還是把程序做好,讓程序本身深入人心,別人喜歡你了,就算你被幹掉了,他們也會主動的把你拉起來,然後把你加入他們的白名單,然後我們的目的就實現了不是 😁 ~