閒話
公司接了項目,開發一個在線升級功能,其中我需要實現手機端與PC端的通信。公司選擇使用MTP來實現這個需求,因此我分析了大量的關於MTP的代碼,從frameworks層到app,再到JNI層。鑑於網上關於這樣的文章太少,而我開發的過程也比較長,因此我決定把framework, app , JNI層的分析都寫下來,希望能幫助開發類似功能的小夥伴。
鄧凡平老師寫的深入理解Android系列書籍,有的已經不出版了,但是他在博客中把文章的所有內容都發布出來,他說知識需要傳遞。這一點,我深感佩服。
服務開啓
UsbService是一個系統服務,它在system_server進程中創建並註冊的。
private static final String USB_SERVICE_CLASS =
"com.android.server.usb.UsbService$Lifecycle";
private void startOtherServices() {
// ...
mSystemServiceManager.startService(USB_SERVICE_CLASS);
// ...
}
SystemServiceManager通過反射創建UsbService$Lifecycle對象(Lifecycle是UsbService的一個內部類),然後加入到List集合中,最後調用Lifcycle對象的onStart方法。
SystemServiceManager保存了各種服務,並且會把系統啓動的各個階段告訴服務,我們可以看看UsbService$Lifecycle的各種生命週期回調
public class UsbService extends IUsbManager.Stub {
public static class Lifecycle extends SystemService {
// 服務創建階段
@Override
public void onStart() {
mUsbService = new UsbService(getContext());
publishBinderService(Context.USB_SERVICE, mUsbService);
}
// 響應系統啓動階段
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
// 系統就緒階段
mUsbService.systemReady();
} else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
// 系統啓動完畢
mUsbService.bootCompleted();
}
}
}
}
可以看到,一個服務會經過創建階段,系統就緒階段,系統啓動完畢階段。接下來,分爲三部分來分析UsbService的啓動過程。
服務創建階段
在服務創建階段,首先創建了UsbService一個對象,由於UsbService是一個Binder對象,然後就把這個服務發佈到ServiceManager。發佈這個服務後,客戶端就可以訪問這個服務。
現在來看下UsbService的構造函數
public UsbService(Context context) {
// ...
if (new File("/sys/class/android_usb").exists()) {
mDeviceManager = new UsbDeviceManager(context, mAlsaManager, mSettingsManager);
}
// ...
}
在構造函數中,與MTP相關的主要代碼就是創建UsbDeviceManager對象。
MTP模式下,Android設備是作爲Device端,UsbDeviceManager就是用來處理Device端的事務。
現在來看下UsbDeviceManager的構造函數做了什麼
public UsbDeviceManager(Context context, UsbAlsaManager alsaManager,
UsbSettingsManager settingsManager) {
// ...
// 我的項目不支持MTP的Hal層
if (halNotPresent) {
// 1. 初始化mHandler
mHandler = new UsbHandlerLegacy(FgThread.get().getLooper(), mContext, this,
alsaManager, settingsManager);
} else {
// ...
}
// 2. 註冊各種廣播
//... 這裏其實註冊了很多個廣播接收器(只不過和省略代碼了)
BroadcastReceiver chargingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharging = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
mHandler.sendMessage(MSG_UPDATE_CHARGING_STATE, usbCharging);
}
};
mContext.registerReceiver(chargingReceiver,
new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
// 3. 監聽USB狀態改變
mUEventObserver = new UsbUEventObserver();
mUEventObserver.startObserving(USB_STATE_MATCH);
mUEventObserver.startObserving(USB_STATE_MATCH_SEC);
mUEventObserver.startObserving(ACCESSORY_START_MATCH);
}
UsbDeviceManager的構造函數做了三件事。
第一件事,初始化mHandler對象。由於我的項目不支持MTP的Hal層,因此mHandler的初始化使用的是UsbHandlerLegacy對象。
第二事,註冊了各種廣播接收器,例如端口變化,語言變化,等等。這裏我把關於充電的廣播接收器代碼展示出來了。當我們把手機通過USB線連接到電腦端的時候,手機會充電,並且手機上會出現一個關於USB充電的通知。打開這個關於USB的通知,我們就可以切換USB的功能,例如MTP, PTP,等等。
第三件事,通過Linux Uevent機制監聽USB狀態變換。當手機通過USB線連接電腦時,USB狀態會從DISCONNECTED變爲CONNECTED,再變爲CONFIGURED。當狀態改變會處理usb狀態更新操作,這個過程在後面會分析到。
現在來看看UsbHandlerLegacy對象的創建。
UsbHandlerLegacy(Looper looper, Context context, UsbDeviceManager deviceManager,
UsbAlsaManager alsaManager, UsbSettingsManager settingsManager) {
// 父類構造函數初始化了一些參數,大家可以根據需要自己分析
super(looper, context, deviceManager, alsaManager, settingsManager);
try {
// 1. 讀取oem覆蓋配置
readOemUsbOverrideConfig(context);
// 2. 讀取各種屬性的值
// 2.1 正常模式,讀取的是persist.sys.usb.config屬性的值
mCurrentOemFunctions = getSystemProperty(getPersistProp(false),
UsbManager.USB_FUNCTION_NONE);
// ro.bootmode屬性的值爲normal或unknown,就表示正常啓動
if (isNormalBoot()) {
// 2.2 讀取sys.usb.config屬性的值,這個屬性表示當前設置的usb功能
mCurrentFunctionsStr = getSystemProperty(USB_CONFIG_PROPERTY,
UsbManager.USB_FUNCTION_NONE);
// 2.3 比較sys.usb.config屬性與sys.usb.state屬性的值
// sys.usb.state屬性表示usb的實際功能
// 如果兩個屬性相等,表示usb設置的功能都起效了
mCurrentFunctionsApplied = mCurrentFunctionsStr.equals(
getSystemProperty(USB_STATE_PROPERTY, UsbManager.USB_FUNCTION_NONE));
} else {
}
// mCurrentFunctions代表當前要設置的usb功能,初始值爲0
mCurrentFunctions = UsbManager.FUNCTION_NONE;
mCurrentUsbFunctionsReceived = true;
// 3. 讀取一次usb狀態,然後做一次更新操作
String state = FileUtils.readTextFile(new File(STATE_PATH), 0, null).trim();
updateState(state);
} catch (Exception e) {
Slog.e(TAG, "Error initializing UsbHandler", e);
}
}
UsbHandlerLegacy的構造函數大致分爲三步。首先看第一步,讀取oem廠商的關於usb功能的覆蓋配置。
private void readOemUsbOverrideConfig(Context context) {
// 數組每一項的格式爲[bootmode]:[original USB mode]:[USB mode used]
String[] configList = context.getResources().getStringArray(
com.android.internal.R.array.config_oemUsbModeOverride);
if (configList != null) {
for (String config : configList) {
String[] items = config.split(":");
if (items.length == 3 || items.length == 4) {
if (mOemModeMap == null) {
mOemModeMap = new HashMap<>();
}
HashMap<String, Pair<String, String>> overrideMap =
mOemModeMap.get(items[0]);
if (overrideMap == null) {
overrideMap = new HashMap<>();
mOemModeMap.put(items[0], overrideMap);
}
// Favoring the first combination if duplicate exists
if (!overrideMap.containsKey(items[1])) {
if (items.length == 3) {
overrideMap.put(items[1], new Pair<>(items[2], ""));
} else {
overrideMap.put(items[1], new Pair<>(items[2], items[3]));
}
}
}
}
}
}
讀取的是config_oemUsbModeOverride
數組,然後保存到mOemModeMap中。數組每一項的格式爲[bootmode]:[original USB mode]:[USB mode used]
,保存的格式可以大致描述爲HashMap<bootmode, HashMap<original_usb_mode, Pair<usb_mode_used, "">
。我的項目中,這個數組爲空。
然後第二步,讀取了各種屬性值(只考慮正常啓動模式),如下。
- mCurrentOemFunctions的值是
persist.sys.usb.config
屬性的值。按照源碼註釋,這個屬性值存儲了adb的開啓狀態(如果開啓了adb,那麼這個值會包含adb字符串)。另外,源碼註釋說這個屬性也可以運營商定製的一些功能,但是隻用於測試目的。 - mCurrentFunctionsStr的值是
sys.usb.config
屬性值。這個屬性表示當前設置的usb功能的值。在日常工作中,我們可以通過adb shell命令設置這個屬性值來切換usb功能,例如adb shell setprop sys.usb.config mtp,adb
可以切換到mtp功能。 - 如果通過
sys.usb.config
屬性切換功能成功,那麼sys.usb.state
屬性值就與sys.usb.config
屬性值一樣。也就是說sys.usb.state
代表usb的實際功能的值。所以,可以通過比較這兩個屬性值來判斷usb所有功能是否切換成功,如果成功了,mCurrentFunctionsApplied的值爲1,否則爲0。
第三步,讀取了當前usb狀態,並且做了一次更新操作。更新操作會發送相關通知,以及發送廣播,但是現在處理服務創建階段,這個操作都無法執行,因此這裏不做分析。但是當處理系統就緒階段或系統啓動完畢階段,就可以做相應的操作,在後面的分析中可以看到。
系統就緒階段
根據前面的代碼,在系統就緒階段,會調用UsbService的systemRead()方法,然後轉到UsbDeviceManager的systemRead()方法
public void systemReady() {
// 註冊一個關於屏幕狀態的回調,有兩個方法
LocalServices.getService(ActivityTaskManagerInternal.class).registerScreenObserver(this);
mHandler.sendEmptyMessage(MSG_SYSTEM_READY);
}
首先註冊了一個關於屏幕的回調,這個回調用於處理在安全鎖屏下,設置usb的功能。但是這個功能好像處於開發階段,只能通過adb shell
命令操作,通過輸入adb shell svc usb
可以查看使用幫助。
接下來,發送了一個消息MSG_SYSTEM_READY
,我們來看下這個消息是如何處理的
case MSG_SYSTEM_READY:
// 獲取到notification服務接口
mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
// 向adb service註冊一個回調,用於狀態adb相關的狀態
LocalServices.getService(
AdbManagerInternal.class).registerTransport(new AdbTransport(this));
// Ensure that the notification channels are set up
if (isTv()) {
// ...
}
// 設置系統就緒的標誌位
mSystemReady = true;
// 此時系統還沒有啓動完成,這裏沒有做任何事
// 這應該是歷史原因造成的代碼冗餘
finishBoot();
break;
可以看到,在系統就緒階段才獲取到了通知服務的接口,這也從側面證明了在UsbService創建階段,是無法發送通知的。然而我卻有點疑惑,通知服務和UsbService同進程,並且通知服務也有內部接口可供系統服務調用,爲何這裏還要通過NotificationManager發送廣播?難道只是爲寫代碼方便,但是這樣一來,不就進行了一次不必要的Binder通信(可能這是我的妄言!)?
獲取通知服務接口後,就向adb服務註冊了一個回調,可以通過這個回調,可以接收到關於adb開戶/關閉的消息。
系統啓動完畢階段
現在來看下最後一個階段,系統啓動完畢階段。根據前面的代碼,會調用UsbService的bootcompleted()方法,然後調用UsbDeviceManager的bootcompleted()方法
public void bootCompleted() {
mHandler.sendEmptyMessage(MSG_BOOT_COMPLETED);
}
只是發送了一條消息,看下消息如何處理的
case MSG_BOOT_COMPLETED:
// 設置系統啓動完成的標誌
mBootCompleted = true;
finishBoot();
break;
很簡單,設置了一個啓動標誌,然後就調用finishBoot()方法完成最後的任務
protected void finishBoot() {
if (mBootCompleted && mCurrentUsbFunctionsReceived && mSystemReady) {
// mPendingBootBroadcast是在服務創建階段設置的
if (mPendingBootBroadcast) {
// 1. 發送/更新usb狀態改變的廣播
updateUsbStateBroadcastIfNeeded(getAppliedFunctions(mCurrentFunctions));
mPendingBootBroadcast = false;
}
if (!mScreenLocked
&& mScreenUnlockedFunctions != UsbManager.FUNCTION_NONE) {
// 這個功能還是處於調試階段,不分析
setScreenUnlockedFunctions();
} else {
// 2. 設置USB功能爲NONE
setEnabledFunctions(UsbManager.FUNCTION_NONE, false);
}
// 關於Accessory功能
if (mCurrentAccessory != null) {
mUsbDeviceManager.getCurrentSettings().accessoryAttached(mCurrentAccessory);
}
// 3. 如果手機已經連接電腦就發送usb通知,通過這個通知,可以選擇usb模式
updateUsbNotification(false);
// 4. 如果adb已經開啓,並且手機已經連接電腦,就發送adb通知
updateAdbNotification(false);
// 關於MIDI功能
updateUsbFunctions();
}
}
如果現在手機沒有通過USB線連接電腦,那麼第一步的發送USB狀態廣播,第三步的USB通知,第四步adb通知,都無法執行。唯一能執行的就是第二步,設置USB功能爲NONE。
OK,現在終於到最關鍵的一步,設置USB功能,它調用的是setEnabledFunctions()
方法。這個方法本身是一想抽象方法,在我的項目中,實現類爲UsbHandlerLegacy
protected void setEnabledFunctions(long usbFunctions, boolean forceRestart) {
// 判斷數據是否解鎖,只有MTP和PTP的數據是解鎖的
boolean usbDataUnlocked = isUsbDataTransferActive(usbFunctions);
// 處理數據解鎖狀態改變的情況
if (usbDataUnlocked != mUsbDataUnlocked) {
// 更新數據解鎖狀態
mUsbDataUnlocked = usbDataUnlocked;
// 更新usb通知
updateUsbNotification(false);
// forceRestart設置爲true,表示需要強制重啓usb功能
forceRestart = true;
}
// 在設置新usb功能前,先保存舊的狀態,以免設置新功能失敗,還可以恢復
final long oldFunctions = mCurrentFunctions;
final boolean oldFunctionsApplied = mCurrentFunctionsApplied;
// 嘗試設置usb新功能
if (trySetEnabledFunctions(usbFunctions, forceRestart)) {
return;
}
// 如果到這裏,就表示新功能設置失敗,那麼就回退之前的狀態
if (oldFunctionsApplied && oldFunctions != usbFunctions) {
Slog.e(TAG, "Failsafe 1: Restoring previous USB functions.");
if (trySetEnabledFunctions(oldFunctions, false)) {
return;
}
}
// 如果回退還是失敗了,那麼就設置usb功能爲NONE
if (trySetEnabledFunctions(UsbManager.FUNCTION_NONE, false)) {
return;
}
// 如果設置NONE還是失敗了,那麼再試一次設置NONE
if (trySetEnabledFunctions(UsbManager.FUNCTION_NONE, false)) {
return;
}
// 如果走到這裏,就表示異常了。
Slog.e(TAG, "Unable to set any USB functions!");
}
首先判斷要設置的新的USB功能的數據是否是解鎖狀態,只有MTP和PTP模式的數據是解鎖狀態,這是爲何你能在設置MTP或PTP模式後,在PC端能看到手機中的文件,然而這個文件只是手機內存中文件的映射,並不是文件本身。
然後處理數據解鎖狀態改變的情況,如果是,那麼會更新狀態,更新usb廣播,然後最重要的是設置forceRestart
變量的值爲true
,這個變量代表要強制重啓usb功能。
最後,設置新usb功能。如果失敗了,就回退。現在來看下trySetEnabledFunctions()
方法如何設置新功能
private boolean trySetEnabledFunctions(long usbFunctions, boolean forceRestart) {
// 1. 把新usb功能轉化爲字符串
String functions = null;
// 如果新功能不是NONE,就轉化
if (usbFunctions != UsbManager.FUNCTION_NONE) {
functions = UsbManager.usbFunctionsToString(usbFunctions);
}
// 保存待設置的新usb功能
mCurrentFunctions = usbFunctions;
// 如果轉化後的功能爲空,那麼就從其它地方獲取
if (functions == null || applyAdbFunction(functions)
.equals(UsbManager.USB_FUNCTION_NONE)) {
// 獲取persist.sys.usb.config屬性值
functions = getSystemProperty(getPersistProp(true),
UsbManager.USB_FUNCTION_NONE);
// 如果persist.sys.usb.config屬性值還是爲NONE
if (functions.equals(UsbManager.USB_FUNCTION_NONE))
// 如果adb開啓,返回adb,否則返回mtp
functions = UsbManager.usbFunctionsToString(getChargingFunctions());
}
// adb開啓,就追加adb值,否則移除adb值
functions = applyAdbFunction(functions);
// 2. 獲取oem覆蓋的usb功能
String oemFunctions = applyOemOverrideFunction(functions);
// 處理非正常啓動模式情況,忽略
if (!isNormalBoot() && !mCurrentFunctionsStr.equals(functions)) {
setSystemProperty(getPersistProp(true), functions);
}
// 3. 設置新功能
if ((!functions.equals(oemFunctions)
&& !mCurrentOemFunctions.equals(oemFunctions))
|| !mCurrentFunctionsStr.equals(functions)
|| !mCurrentFunctionsApplied
|| forceRestart) {
Slog.i(TAG, "Setting USB config to " + functions);
// 保存要設置新功能對應的字符串值
mCurrentFunctionsStr = functions;
// 保存oem覆蓋功能的字符串值
mCurrentOemFunctions = oemFunctions;
mCurrentFunctionsApplied = false;
// 先斷開已經存在的usb連接
setUsbConfig(UsbManager.USB_FUNCTION_NONE);
// 判斷是否成功
if (!waitForState(UsbManager.USB_FUNCTION_NONE)) {
Slog.e(TAG, "Failed to kick USB config");
return false;
}
// 設置新功能,注意,這裏使用的是oem覆蓋的功能
setUsbConfig(oemFunctions);
// 如果新功能包含mtp或ptp,那麼就要更新usb狀態改變廣播
// 廣播接收者會映射主內存的文件到PC端
if (mBootCompleted
&& (containsFunction(functions, UsbManager.USB_FUNCTION_MTP)
|| containsFunction(functions, UsbManager.USB_FUNCTION_PTP))) {
updateUsbStateBroadcastIfNeeded(getAppliedFunctions(mCurrentFunctions));
}
// 等待新功能設置完畢
if (!waitForState(oemFunctions)) {
Slog.e(TAG, "Failed to switch USB config to " + functions);
return false;
}
mCurrentFunctionsApplied = true;
}
return true;
}
我把這裏的邏輯分爲了三步.
第一步,把待設置的USB功能轉化爲字符串,有兩種情況
- 如果新功能爲
FUNCTION_NONE
,那麼轉化後的值從persist.sys.usb.config
獲取,如果獲取值爲NONE,就判斷adb是否開啓,如果開啓了,轉化後的值爲adb,如果沒有開啓,轉化後的值爲mtp。前面分析說過,persist.sys.usb.config
主要包含用於判斷adb是否開啓在值,然後還包含一些廠商定製且用於測試目的的功能。例如,高通項目,這個值可能爲adb,diag
,這個diag就是高通自己的功能。 - 如果新功能不爲
FUNCTION_NONE
,把直接轉化。例如新功能爲FUNCTION_MTP
,那麼轉化後的字符串爲mtp
。
轉化字符串後,根據adb是否開啓,來決定從轉化後的字符串中增加adb屬性還是移除adb屬性。
第二步,獲取oem覆蓋的功能。前面說過,默認系統是沒有使用覆蓋功能,所以這裏獲取的覆蓋後的功能與新功能轉化後的字符串是一樣的。
我在分析代碼的時候,腦海裏一直在想,這個覆蓋功能如何使用。根據我的對代碼的分析,唯一的規則就是主要功能不能覆蓋。舉個例子,如果新設置的功能的字符串爲mtp,那麼覆蓋數組中的其中一項元素的值應該是normal:mtp:mtp,diag
,其中nomral表示正常啓動,mtp表示原始的功能,mtp,diag表示覆蓋後的功能,請注意,覆蓋後的功能一定要保存mtp這個主功能。當然這只是我個人對代碼分析得出的結論,還沒驗證。這裏我要吐槽一下這個功能的設計者,難道寫個例子以及注意事項就這麼難嗎?
第三步,設置新功能。不過設置新功能前,首先要斷開已經存在的連接,然後再設置新功能。設置新功能是通過setUsbConfig()
方法,來看下實現
private void setUsbConfig(String config) {
// 設置sys.usb.config
setSystemProperty(USB_CONFIG_PROPERTY, config);
}
震驚!原來就是設置sys.usb.config的屬性值,還記得嗎,在前面的分析中,也解釋過這個屬性值,它就是代表當前設置的usb功能,從這裏就可以得到證明。
這其實也在提示我們,其實可以通過adb shell setprop
命令設置這個屬性,從而控制usb功能的切換。在實際的工作中,屢試不爽。
設置這個屬性後如何判斷設置成功了呢?這就是waitForState()
所做的
private boolean waitForState(String state) {
String value = null;
for (int i = 0; i < 20; i++) {
// 獲取sys.usb.stat值
value = getSystemProperty(USB_STATE_PROPERTY, "");
// 與剛纔設置的sys.usb.config屬性值相比較
if (state.equals(value)) return true;
SystemClock.sleep(50);
}
return false;
}
說實話,我看到這段代碼,確實吃了一鯨! 這段代碼在1秒內執行20次,獲取sys.usb.state
屬性值,然後與設置的sys.usb.config
屬性值相比較,如果相等就表示功能設置成功。
還記得嗎?在前面的分析中,我也解釋過sys.usb.state
屬性的作用,它代表usb實際的功能,從這裏就可以得到驗證。
那麼現在有一個問題,底層如何實現usb功能切換呢?當然是響應屬性sys.usb.config
屬性改變,例如我的項目的底層呼應代碼是這樣
on property:sys.usb.config=mtp,adb && property:sys.usb.configfs=0
# 先寫0
write /sys/class/android_usb/android0/enable 0
# 寫序列號
write /sys/class/android_usb/android0/iSerial ${ro.serialno}
# 寫vid, pid
write /sys/class/android_usb/android0/idVendor 05C6
write /sys/class/android_usb/android0/idProduct 9039
# 設置USB功能爲mtp,adb
write /sys/class/android_usb/android0/functions mtp,adb
# 再寫1啓動功能
write /sys/class/android_usb/android0/enable 1
# 啓動adb
start adbd
# 設置 sys.usb.state屬性值爲sys.usb.config的屬性值
setprop sys.usb.state ${sys.usb.config}
根據註釋,你應該就可以很清楚瞭解這個過程了。
so, 你以爲這就完了嗎?還沒呢,如果新設置的功能是MTP或PTP,那麼還要更新廣播呢。
protected void updateUsbStateBroadcastIfNeeded(long functions) {
// send a sticky broadcast containing current USB state
Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
| Intent.FLAG_RECEIVER_FOREGROUND);
// 保存了usb狀態值
intent.putExtra(UsbManager.USB_CONNECTED, mConnected);
intent.putExtra(UsbManager.USB_HOST_CONNECTED, mHostConnected);
intent.putExtra(UsbManager.USB_CONFIGURED, mConfigured);
intent.putExtra(UsbManager.USB_DATA_UNLOCKED,
isUsbTransferAllowed() && isUsbDataTransferActive(mCurrentFunctions));
// 保存了要設置的新功能的值,例如設置的是MTP,那麼參數的key爲mtp,值爲true
long remainingFunctions = functions;
while (remainingFunctions != 0) {
intent.putExtra(UsbManager.usbFunctionsToString(
Long.highestOneBit(remainingFunctions)), true);
remainingFunctions -= Long.highestOneBit(remainingFunctions);
}
// 如果狀態沒有改變,就不發送廣播
if (!isUsbStateChanged(intent)) {
return;
}
// 注意這裏發送的是一個sticky廣播
sendStickyBroadcast(intent);
mBroadcastedIntent = intent;
注意,這裏發送的是一個sticky廣播。那麼這個廣播的接收者是誰呢?接收這個廣播又做了什麼呢?這就是下一篇文章的內容。
結束
UsbService是usb協議的實現,例如MTP,PTP構建於usb協議上,UsbService就實現了。然而本文是以MTP爲主線進行分析的,篇幅不小,但是如果你要開發或定製關於usb功能時,這篇文章不容錯過。