這幾天錘子科技新聞不斷,成都市政府投資錘子科技6個億,這也許是錘子科技要在成都建研發(fā)中心的一個重要緣由。而錘子科技沒有落戶在軟件研發(fā)公司和人員聚集的高新區(qū)天府軟件園,讓我覺得有些“可惜”,可能老羅對錘子科技的定位超出我的想象。

政府的投資是否會有利于錘子科技的發(fā)展不得而知,不過段子倒是開始滿天飛了,來一段吧(可能需要熟悉四川話的才好理解笑點):
秘書問市長:“市長,我們還有6億的市政基金,要投資什么嗎?”
市長正忙著呢,回:“投個錘子!”
“哦”,秘書就撥通了老羅的電話。
回到正題,前兩周我參加了兩次錘子科技的面試,在市場上錘子手機比較推崇“工匠精神”,在面試上他們也比較精益求精,面試官是我遇到的能感受到技術(shù)和問問題都很不錯的人。
有些問題上被問得也有點“尷尬”,細細思考也是自己沒有做好“工匠”的事情,很多問題都沒有認真深入,雖然我也一直在強調(diào),不過我也只深入了一部分。
今天就來和大家談談,我在面試錘子科技時和面試官討論的一道和Binder線程池相關(guān)的問題。
面試題:多個進程同時調(diào)用一個ContentProvider的query獲取數(shù)據(jù),ContentPrvoider是如何反應的呢?
我們知道像Activity這樣的組件,它的生命周期回調(diào)函數(shù)是在UI線程中運行的,ContentProvider的onCreate()也是在UI線程運行,回答這個面試題前,我們要搞清楚ContentProvider的query(),insert(),delete(),update()這幾個方法是在UI線程中運行的嗎?
如果是,那么就等于說在這里做長時間的操作的話,就會有ANR的問題。如果不是,那是在一個工作線程還是多個線程中呢,即ContentProvider是否支持并發(fā)操作?
ContentResolver與ContentProvider類隱藏了實現(xiàn)細節(jié),但是ContentProvider所提供的query(),insert(),delete(),update()都是在ContentProvider進程的線程池中被調(diào)用執(zhí)行的,而不是進程的主線程中。因為那些方法可能同時被多個線程所調(diào)用,所以他們都應該是線程安全的。
ContentProvider實現(xiàn)了進程間通信也是基于Binder機制的,所以會回到Binder的線程處理問題。并不是每個ContentProvider都會有一個線程池,而一個進程會共有一個線程池,其實就是Binder線程池。
我之前看到過說明是,每個ContentProvider會有線程池在管理每個客戶端ContentResolver的請求,每個線程池有16個線程,不過后來也一直沒有在官網(wǎng)上找到資料印證。
實驗
當我聊到這個16的數(shù)量問題時,面試官有一些疑問。需要我對這個數(shù)量的由來做一個說明。
如何證明這個線程池確實是16個線程呢?
有兩種方法:
- 直接查看ContentProvider或者Binder的相關(guān)源碼證實;
- 直接寫一個應用Demo來證實。
我們來試試第2種方式,現(xiàn)在寫一個簡單的測試Demo,一個ContentProvider(里面操作數(shù)據(jù)的地方,我們讓它sleep 10秒鐘),一個調(diào)用者Activity同時發(fā)送幾十個請求。在ContentProvider的方法中打出Log看運行在哪個線程,以及這幾十個請求的執(zhí)行情況。
ContentProvider代碼:
public class MyContentProvider extends ContentProvider {
private final static String TAG = "MyContentProvider";
@Override
public boolean onCreate() {
Log.i(TAG, "onCreate() threadName= " + Thread.currentThread().getName()
+ " threadId=" + Thread.currentThread().getId());
return false;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.i(TAG, "insert() threadName= " + Thread.currentThread().getName()
+ " threadId=" + Thread.currentThread().getId());
try {
Thread.sleep(10000); // 模擬耗時工作
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
......
調(diào)用方Activity的代碼:
public class MainActivity extends AppCompatActivity {
private final static String TAG = "MainActivity";
public static final Uri CONTENT_URI = Uri.parse("content://net.goeasyway.myprovider");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < 50; i++) {
new Thread(new Runnable() {
@Override
public void run() {
getContentResolver().insert(CONTENT_URI, new ContentValues());
}
}).start();
}
}
}
測試結(jié)果分兩種情況:
1、同一進程
ContentProvider和調(diào)用getContentResolver()在同一進程中的情況,即AndroidManifest.xml這樣配置provider標簽的:
<provider
android:authorities="net.goeasyway.myprovider"
android:name=".MyContentProvider" />
測試結(jié)果發(fā)現(xiàn),ContentProvider.insert()會生成一個新的線程去處理請求,所以getContentResolver().insert()請求了50次,就會ContentProvider的進程中產(chǎn)生50個新的線程去處理insert()操作。
I/MyContentProvider: insert() threadName= Thread-1 threadId=1304
I/MyContentProvider: insert() threadName= Thread-3 threadId=1306
I/MyContentProvider: insert() threadName= Thread-2 threadId=1305
I/MyContentProvider: insert() threadName= Thread-4 threadId=1307
I/MyContentProvider: insert() threadName= Thread-5 threadId=1308
I/MyContentProvider: insert() threadName= Thread-6 threadId=1309
I/MyContentProvider: insert() threadName= Thread-7 threadId=1310
I/MyContentProvider: insert() threadName= Thread-8 threadId=1311
I/MyContentProvider: insert() threadName= Thread-9 threadId=1312
......
I/MyContentProvider: insert() threadName= Thread-47 threadId=1350
I/MyContentProvider: insert() threadName= Thread-49 threadId=1352
I/MyContentProvider: insert() threadName= Thread-48 threadId=1351
I/MyContentProvider: insert() threadName= Thread-50 threadId=1353
2、不同進程
ContentProvider和調(diào)用getContentResolver()不在同一進程中的情況,配置provider標簽時給它調(diào)置一個遠程進程:
<provider
android:authorities="net.goeasyway.myprovider"
android:name=".MyContentProvider"
android:process=":remote"/>
這樣配置MyContentProvider將運行在“包名:remote”的進程中,和MainActivity所在進程“包名”之間進程間通信。測試的結(jié)果發(fā)現(xiàn)MyContentProvider中的insert()方法運行在Binder線程池的線程中。
I/MyContentProvider: insert() threadName= Binder:10436_1 threadId=1285
I/MyContentProvider: insert() threadName= Binder:10436_2 threadId=1286
I/MyContentProvider: insert() threadName= Binder:10436_3 threadId=1288
I/MyContentProvider: insert() threadName= Binder:10436_4 threadId=1289
I/MyContentProvider: insert() threadName= Binder:10436_5 threadId=1290
I/MyContentProvider: insert() threadName= Binder:10436_6 threadId=1291
I/MyContentProvider: insert() threadName= Binder:10436_7 threadId=1292
I/MyContentProvider: insert() threadName= Binder:10436_8 threadId=1293
I/MyContentProvider: insert() threadName= Binder:10436_9 threadId=1294
I/MyContentProvider: insert() threadName= Binder:10436_A threadId=1295
I/MyContentProvider: insert() threadName= Binder:10436_B threadId=1296
I/MyContentProvider: insert() threadName= Binder:10436_C threadId=1297
I/MyContentProvider: insert() threadName= Binder:10436_D threadId=1298
I/MyContentProvider: insert() threadName= Binder:10436_E threadId=1299
I/MyContentProvider: insert() threadName= Binder:10436_F threadId=1300
I/MyContentProvider: insert() threadName= Binder:10436_10 threadId=1301
......
每次MyContentProvider的insert()只能在16個Binder線程中執(zhí)行,只有等這16個線程有空閑之后才后處理剩下的getContentResolver().insert()的請求。
筆者經(jīng)驗分享:雖然去研究ContentProvider的啟動和調(diào)用流程代碼肯定也能得出上面的結(jié)論,但還是會有一些難度。所以,工作中有時候想太多還不如直接動手試一下,結(jié)論一下子就出來了。
和面試官的“爭論”
這個結(jié)論是不是有問題呢?
當我聊到這個線程池中有16個線程可供使用時,面試官對這個結(jié)論提出了一些看法。一個進程創(chuàng)建后為了和在系統(tǒng)進程中的AMS(ActivityManangerService)通信,一個APK進程本身就是在Binder線程池中占用2個線程來維持Binder的通信。
那么,上面提到的第二種不同進程的案例中,ContentProvider能同時使用的線程數(shù)量應該是14個才對啊。怎么我們測試的結(jié)果會是16呢?
這一點我當時和面試官是有分歧的。因為我記得做過實驗結(jié)果就是16個線程,但如過一個進程只維護一個Binder線程池的話,確實應該是14個線程才對。
在這點上,我確實沒有理解清楚Binder的線程池,所以我知道了實際效果可以同時運行16個線程(假設自己的實驗沒出錯),但卻無法解釋面試官提的“為什么不是14個的問題”。
我面試回來后,看一些說明和代碼片段,一個初步的判斷是,面試官提到的和AMS通信用到的線程并不會算在這個線程池中。
歡迎提出異議或者你認為正確的解釋。
高級中的高級
可能很多讀者會覺得面試官這樣問有點吹毛求疵了,這不是為難人嗎?
錘子科技把高級工程師還進行了細分,即高級中再來一個“初級、中級、高級”這類的級別,很多公司應該也有類似定級別的(比如有喜歡P的、有喜歡T的)。高級中的高級和高級中的初級,你覺得會差在哪里呢?
所以,作為面試者你應該仔細思考一下,很多時候面試官過問的細節(jié)往往就是差距所在。
“標準答案”
面試題:多個進程同時調(diào)用一個ContentProvider的query獲取數(shù)據(jù),ContentPrvoider是如何反應的呢?
標準答案:一個content provider可以接受來自另外一個進程的數(shù)據(jù)請求。盡管ContentResolver與ContentProvider類隱藏了實現(xiàn)細節(jié),但是ContentProvider所提供的query(),insert(),delete(),update()都是在ContentProvider進程的線程池中被調(diào)用執(zhí)行的,而不是進程的主線程中。這個線程池是有Binder創(chuàng)建和維護的,其實使用的就是每個應用進程中的Binder線程池。
面試題:你覺得Android設計ContentProvider的目的是什么呢?
標準答案:1. 隱藏數(shù)據(jù)的實現(xiàn)方式,對外提供統(tǒng)一的數(shù)據(jù)訪問接口;
2.更好的數(shù)據(jù)訪問權(quán)限管理。ContentProvider可以對開發(fā)的數(shù)據(jù)進行權(quán)限設置,不同的URI可以對應不同的權(quán)限,只有符合權(quán)限要求的組件才能訪問到ContentProvider的具體操作。
3.ContentProvider封裝了跨進程共享的邏輯,我們只需要Uri即可訪問數(shù)據(jù)。由系統(tǒng)來管理ContentProvider的創(chuàng)建、生命周期及訪問的線程分配,簡化我們在應用間共享數(shù)據(jù)(進程間通信)的方式。我們只管通過ContentResolver訪問ContentProvider所提示的數(shù)據(jù)接口,而不需要擔心它所在進程是啟動還是未啟動。
面試題:運行在主線程的ContentProvider為什么不會影響主線程的UI操作?
標準答案:
ContentProvider的onCreate()是運行在UI線程的,而query(),insert(),delete(),update()是運行在線程池中的工作線程的,所以調(diào)用這向個方法并不會阻塞ContentProvider所在進程的主線程,但可能會阻塞調(diào)用者所在的進程的UI線程!
所以,調(diào)用ContentProvider的操作仍然要放在子線程中去做。雖然直接的CRUD的操作是在工作線程的,但系統(tǒng)會讓你的調(diào)用線程等待這個異步的操作完成,你才可以繼續(xù)線程之前的工作。
相關(guān)面試題:
Android面試一天一題(15 Day:ContentProvider)
Android面試一天一題(Day 44:實戰(zhàn)美團--Java內(nèi)存模型)