網站首頁 編程語言 正文
引言
Input系統: 按鍵事件分發 從整體上描繪了通用的事件分發過程,其中有兩個比較的環節,一個是截斷策略,一個是分發策略。Input系統:截斷策略的分析與應用 分析了截斷策略及其應用,本文來分析分發策略及其應用。
在正式開始分析前,讀者務必仔細地閱讀 Input系統: 按鍵事件分發 ,了解截斷策略和分發策略的執行時機。否則,閱讀本文沒有意義,反而是浪費時間。
分發策略原理
根據 Input系統: 按鍵事件分發 可知,分發策略發生在事件分發的過程中,并且發生在事件分發循環前,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
// ...
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) {
// ...
}
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) {
if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) {
if (INPUTDISPATCHER_SKIP_EVENT_KEY != 0) {
// ...
}
// 創建一個命令,當命令被執行的時候,
// 回調 doInterceptKeyBeforeDispatchingLockedInterruptible()
std::unique_ptr<CommandEntry> commandEntry = std::make_unique<CommandEntry>(
&InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible);
sp<IBinder> focusedWindowToken =
mFocusResolver.getFocusedWindowToken(getTargetDisplayId(*entry));
commandEntry->connectionToken = focusedWindowToken;
commandEntry->keyEntry = entry;
// 把剛創建的命令,加入到隊列 mCommandQueue 中
postCommandLocked(std::move(commandEntry));
// 返回 false 等待命令執行
return false; // wait for the command to run
} else {
// ...
}
} else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) {
// ...
}
// ...
// 啟動分發循環,把事件分發給目標窗口
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
如代碼所示,事件在分發給窗口前,會先執行分發策略。而執行分發策略的方式是創建一個命令 CommandEntry,然后保存到命令隊列中。
當命令被執行的時候,會執行 InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible() 函數。
那么,為何要執行分發策略呢?有如下兩點原因
- 截斷事件,給系統一個優先處理事件的機會。
- 實現組合按鍵功能。
例如,導航欄上的 home, app switch 按鍵的功能就是在這里實現的,分發策略會截斷它們。
從 Input系統:截斷策略的分析與應用 可知,截斷策略也可以截斷事件,讓系統優先處理事件。那么截斷策略與分發策略有什么區別呢?
由 Input系統: 按鍵事件分發 可知,截斷策略是處理一些系統級的事件,例如 power 鍵亮滅屏,這些事件的處理必須讓用戶感覺沒有延時。假如 power 鍵的事件是在分發流程中處理的,那么必須等到 power 事件前面的所有事件都處理完畢,才能輪到 power 事件被處理,這就可能讓用戶感覺系統有點不流暢。
而分發策略處理一些優先級相對較低的系統事件,例如 home,app switch 事件。由于分發策略處于分發過程中,因此當一個 app 在發生 anr 期間,無論我們按多少次 home, app switch 按鍵,系統都會沒有響應。
好,回歸正題,如上面代碼所示,為了執行分發策略,創建了一個命令,并保存到命令隊列,然后就返回了。由 Input系統: 按鍵事件分發 可知,返回到了 InputDispatcher 的線程循環,如下
void InputDispatcher::dispatchOnce() {
nsecs_t nextWakeupTime = LONG_LONG_MAX;
{ // acquire lock
std::scoped_lock _l(mLock);
mDispatcherIsAlive.notify_all();
// 1. 如果沒有命令,分發一次事件
if (!haveCommandsLocked()) {
dispatchOnceInnerLocked(&nextWakeupTime);
}
// 2. 執行命令
// 這個命令來自于前一步的事件分發
if (runCommandsLockedInterruptible()) {
// 馬上開始下一次的線程循環
nextWakeupTime = LONG_LONG_MIN;
}
// 處理 ANR ,并返回下一次線程喚醒的時間。
const nsecs_t nextAnrCheck = processAnrsLocked();
nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
if (nextWakeupTime == LONG_LONG_MAX) {
mDispatcherEnteredIdle.notify_all();
}
} // release lock
nsecs_t currentTime = now();
int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
// 3. 線程休眠 timeoutMillis 毫秒
mLooper->pollOnce(timeoutMillis);
}
第1步,執行事件分發,不過事件為了執行分發策略,創建了一個命令并保存到命令隊列中。
第2步,執行命令隊列中的命令。根據前面創建命令時所分析的,會調用如下函數
void InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible(
CommandEntry* commandEntry) {
// 取出命令中保存的按鍵事件
KeyEntry& entry = *(commandEntry->keyEntry);
KeyEvent event = createKeyEvent(entry);
mLock.unlock();
android::base::Timer t;
const sp<IBinder>& token = commandEntry->connectionToken;
// 執行分發策略
nsecs_t delay = mPolicy->interceptKeyBeforeDispatching(token, &event, entry.policyFlags);
if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
ALOGW("Excessive delay in interceptKeyBeforeDispatching; took %s ms",
std::to_string(t.duration().count()).c_str());
}
mLock.lock();
// 分發策略的結果保存到 KeyEntry::interceptKeyResult
if (delay < 0) {
entry.interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_SKIP;
} else if (!delay) {
entry.interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_CONTINUE;
} else {
entry.interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER;
entry.interceptKeyWakeupTime = now() + delay;
}
}
果然,命令在執行時候,為事件 KeyEntry 查詢了分發策略,并把分發策略的結果保存到 KeyEntry::interceptKeyResult。
注意,分發策略最終是由上層執行的,如果要截斷事件,那么需要返回負值,如果不截斷,返回0,如果暫時不知道如何處理事件,那么返回正值。
第2步執行完畢后,會立刻開始下一次的線程循環。如果要理解這一點,需要理解底層的消息機制,讀者可能參考我寫的 深入理解Native層的消息機制。
在下一次線程循環時,執行第1步時,在事件分發給窗口前,需要根據分發策略的結果,對事件做進一步的處理,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
// ...
// 1. 分發策略的結果表示稍后再嘗試分發事件
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) {
// 還沒到超時的時間,計算線程休眠的時間,讓線程休眠
if (currentTime < entry->interceptKeyWakeupTime) {
if (entry->interceptKeyWakeupTime < *nextWakeupTime) {
*nextWakeupTime = entry->interceptKeyWakeupTime;
}
return false; // wait until next wakeup
}
// 重置分發策略的結果,為了再一次查詢分發策略
// 當再次查詢分發策略時,分發策略會給出是否截斷的結果
entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN;
entry->interceptKeyWakeupTime = 0;
}
// Give the policy a chance to intercept the key.
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) {
// 執行分發策略
// ...
} else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) {
// 2. 分發策略的結果表示路過這個事件,也就是丟棄這個事件
// 這里設置了丟棄的原因,下面會根據這個原因,丟棄事件,不會分發給窗口
if (*dropReason == DropReason::NOT_DROPPED) {
*dropReason = DropReason::POLICY;
}
}
// 事件有原因需要丟棄,不執行后面的分發循環
if (*dropReason != DropReason::NOT_DROPPED) {
setInjectionResult(*entry,
*dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED
: InputEventInjectionResult::FAILED);
mReporter->reportDroppedKey(entry->id);
return true;
}
// ...
// 啟動分發循環,把事件分發給目標窗口
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
對各種分發結果的處理如下
- INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER : 上層暫時不知道如何處理這個事件,所以告訴底層等一會再看看。底層收到這個結果,會讓線程休眠指定時間。當時間到了后,會把重置分發策略結果為 INTERCEPT_KEY_RESULT_UNKNOWN,然后再次查詢分發策略,此時分發策略會給出一個明確的結果,到底是截斷還是不截斷。
- INTERCEPT_KEY_RESULT_SKIP :上層截斷了這個事件,因此讓底層跳過這個事件,也就是不丟棄這個事件。
- INTERCEPT_KEY_RESULT_CONTINUE : 源碼中沒有明確處理這個結果,很簡單嘛,那就是繼續后面的事件分發流程。
那么,什么時候上層不知道如何處理一個事件呢?這是為了實現組合鍵的功能。
當第一個按鍵按下時,分發策略不知道用戶到底會不會按下第二個按鍵,因此它會告訴底層再等等吧,底層因此休眠了。
如果在底層休眠期間,如果用戶按下了第二個按鍵,那么成功觸發組合鍵的功能,當底層醒來時,再次為第一個按鍵的事件查詢分發策略,此時分發策略知道第一個按鍵的事件已經觸發了組合鍵功能,因此告訴底層,第一個按鍵事件截斷了,也就是被上層處理了,那么底層就不會分發這第一個按鍵的事件。
如果在底層休眠期間,如果沒有用戶按下了第二個按鍵。當底層醒來時,再次為第一個按鍵的事件查詢分發策略,此時分發策略知道第一個按鍵事件沒有觸發組合鍵的功能,因此告訴底層這個事件不截斷,繼續分發處理吧。
下面以一個具體的組合鍵以例,來理解分發策略,因此讀者務必仔細理解上面所分析的。
分發策略的應用 - 組合鍵
以手機上最常見的截斷組合鍵為例,也就是 電源鍵 + 音量下鍵,來理解分發策略。但是,請讀者務必,先仔細理解上面所分析的。
組合鍵的功能是由 KeyCombinationManager 管理,它在 PhoneWindowManager 的初始化如下
// PhoneWindowManager.java
private void initKeyCombinationRules() {
// KeyCombinationManager 是用來實現組合按鍵功能的類
mKeyCombinationManager = new KeyCombinationManager();
// 配置默認為 true
final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableScreenshotChord);
if (screenshotChordEnabled) {
// 添加 電源鍵 + 音量下鍵 組合按鍵規則
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) {
@Override
void execute() {
mPowerKeyHandled = true;
// 截屏
interceptScreenshotChord();
}
@Override
void cancel() {
cancelPendingScreenshotChordAction();
}
});
}
// ... 省略其它組合鍵的規則
}
很簡單,創建一個規則用于實現截屏,并保存到了 KeyCombinationManager#mRules 中。
當按下電源鍵,首先會經過截斷策略處理,注意不是分發策略
// PhoneWindowManager.java
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
// ...
if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
// 1. 處理按鍵手勢
// 包括組合鍵
handleKeyGesture(event, interactiveAndOn);
}
switch (keyCode) {
// ...
case KeyEvent.KEYCODE_POWER: {
// 2. power 按鍵事件是不傳遞給用戶的
result &= ~ACTION_PASS_TO_USER;
// ..
break;
}
// ...
}
// ...
return result;
}
第2步,截斷策略會截斷電源按鍵事件。
第1步,截斷策略處理按鍵手勢,這其中就包括組合鍵
// PhoneWindowManager.java
private void handleKeyGesture(KeyEvent event, boolean interactive) {
if (mKeyCombinationManager.interceptKey(event, interactive)) {
// handled by combo keys manager.
mSingleKeyGestureDetector.reset();
return;
}
// ...
}
現在來看下 KeyCombinationManager 如何處理截屏功能的第一個按鍵事件,也就是電源事件
boolean interceptKey(KeyEvent event, boolean interactive) {
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
final int keyCode = event.getKeyCode();
final int count = mActiveRules.size();
final long eventTime = event.getEventTime();
// 交互狀態,一般指亮屏的狀態
// 從這里可以看出,組合鍵的功能,必須在交互狀態下執行
if (interactive && down) {
if (mDownTimes.size() > 0) {
// ...
}
if (mDownTimes.get(keyCode) == 0) {
// 1. 記錄按鍵按下的時間
mDownTimes.put(keyCode, eventTime);
} else {
// ignore old key, maybe a repeat key.
return false;
}
if (mDownTimes.size() == 1) {
mTriggeredRule = null;
// 2. 獲取所有與按鍵相關的規則,保存到 mActiveRules
forAllRules(mRules, (rule)-> {
if (rule.shouldInterceptKey(keyCode)) {
mActiveRules.add(rule);
}
});
} else {
// ...
}
} else {
// ...
}
return false;
}
KeyCombinationManager 處理組合鍵的第一個按鍵事件很簡單,保存了按鍵按下的時間,并找到與這個按鍵相關的規則并保存。
由于電源按鍵事件被截斷,當執行到分發策略時,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
// ...
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) {
// ...
}
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) {
if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) {
// ...不被截斷的事件,才會創建命令,用于執行分發策略...
return false; // wait for the command to run
} else {
// 1. 被截斷的事件,繼續后面的分發流程,最終會被丟棄
entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_CONTINUE;
}
} else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) {
// ...
}
// 2. 如果事件被截斷了,就會在這里被丟棄
if (*dropReason != DropReason::NOT_DROPPED) {
setInjectionResult(*entry,
*dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED
: InputEventInjectionResult::FAILED);
mReporter->reportDroppedKey(entry->id);
return true;
}
// ...
// 啟動分發循環,把事件分發給窗口
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
被截斷策略截斷的事件,不會經過分發策略的處理,并且直接被丟棄。這就是窗口為何收不到 power 按鍵事件的根本原因。
截斷的第一個事件,電源事件,已經分析完畢。現在假設用戶在很短的時間內,按鍵下了音量下鍵。經過截斷策略時,仍然首先經過手勢處理,此時 KeyCombinationManager 處理第二個按鍵的過程如下
boolean interceptKey(KeyEvent event, boolean interactive) {
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
final int keyCode = event.getKeyCode();
final int count = mActiveRules.size();
final long eventTime = event.getEventTime();
if (interactive && down) {
if (mDownTimes.size() > 0) {
if (count > 0
&& eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) {
// 第二個按鍵按下超時
forAllRules(mActiveRules, (rule)-> rule.cancel());
mActiveRules.clear();
return false;
} else if (count == 0) { // has some key down but no active rule exist.
return false;
}
}
if (mDownTimes.get(keyCode) == 0) {
// 保存第二個按鍵按下的時間
mDownTimes.put(keyCode, eventTime);
} else {
// ignore old key, maybe a repeat key.
return false;
}
if (mDownTimes.size() == 1) {
// ...
} else {
// Ignore if rule already triggered.
if (mTriggeredRule != null) {
return true;
}
// check if second key can trigger rule, or remove the non-match rule.
forAllActiveRules((rule) -> {
// 需要在規則的時間內按下第二個按鍵,才能觸發規則
if (!rule.shouldInterceptKeys(mDownTimes)) {
return false;
}
Log.v(TAG, "Performing combination rule : " + rule);
// 觸發組合鍵規則
rule.execute();
// 保存已經觸發的規則
mTriggeredRule = rule;
return true;
});
// 清空 mActiveRules,保存已經觸發的規則
mActiveRules.clear();
if (mTriggeredRule != null) {
mActiveRules.add(mTriggeredRule);
return true;
}
}
} else {
// ...
}
return false;
}
根據代碼可知,只有組合鍵的第二個按鍵在規定的時間內按下(150ms),才能觸發規則。對于 電源鍵 + 音量下鍵,就是觸發截屏。
截斷策略在處理按鍵手勢時,現在已經觸發截屏,那么它是否截斷音量下鍵呢?如果音量下鍵不用來掛斷電話,那就不截斷,這段代碼請讀者自行分析。
我們假設音量下鍵沒有被截斷策略截斷,那么當它經過分發策略時,如何處理呢?如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
// ...
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) {
// ...
}
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) {
// 1. 對于不被截斷的事件,創建命令執行分發策略
if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) {
std::unique_ptr<CommandEntry> commandEntry = std::make_unique<CommandEntry>(
&InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible);
sp<IBinder> focusedWindowToken =
mFocusResolver.getFocusedWindowToken(getTargetDisplayId(*entry));
commandEntry->connectionToken = focusedWindowToken;
commandEntry->keyEntry = entry;
postCommandLocked(std::move(commandEntry));
return false; // wait for the command to run
} else {
// ...
}
} else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) {
// ...
}
// ...
// 啟動分發循環,分發事件
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
音量下鍵事件要執行分發策略,分發策略最終由上層的 PhoneWindowManager 實現,如下
// PhoneWindowManager.java
public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
int policyFlags) {
// ...
final long key_consumed = -1;
if (mKeyCombinationManager.isKeyConsumed(event)) {
// 返回 -1,表示截斷事件
return key_consumed;
}
}
// KeyCombinationManager.java
boolean isKeyConsumed(KeyEvent event) {
if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
return false;
}
// 在觸發組合鍵功能時,mTriggeredRule 保存了觸發的規則
return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode());
}
由于已經觸發了截屏功能,因此分發策略對音量下鍵的處理結果是 -1,也就是截斷它。
底層收到這個截斷信息時,就會丟棄音量下鍵這個事件,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
// ...
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) {
// ...
}
// Give the policy a chance to intercept the key.
if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) {
// ...
} else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) {
// 1. 分發策略的結果是事件被截斷
if (*dropReason == DropReason::NOT_DROPPED) {
*dropReason = DropReason::POLICY;
}
}
// 2. 丟棄被截斷的事件
if (*dropReason != DropReason::NOT_DROPPED) {
setInjectionResult(*entry,
*dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED
: InputEventInjectionResult::FAILED);
mReporter->reportDroppedKey(entry->id);
return true;
}
// ...
// 啟動分發循環,發送事件給窗口
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
由于音量下鍵事件被丟棄,因此窗口也收不到這個事件。其實,組合鍵功能只要觸發,兩個按鍵事件,窗口都收不到。
截屏功能不是只能通過 電源鍵 + 音量下鍵 觸發,還可以通過 音量下鍵 + 電源鍵觸發,但是分析過程卻和上面不一樣。如果音量下鍵先按,那么分發策略會返回一個稍后再試的結果,如果讀者有興趣,可以自行分析。
結束
通過學習本文,我們要達到學以致用的目的,其實最主要的,就是要學會如何自定義組合鍵。對于硬件上新增的按鍵事件,如果要截斷,可以在截斷策略,也可以在分發策略,根據自己所認為的重要性級別來決定。
原文鏈接:https://juejin.cn/post/7195577258906878007
相關推薦
- 2022-07-19 在 NgModule 里通過依賴注入的方式注冊服務實例
- 2022-06-26 ASP.NET?Core中引用OpenAPI服務的添加示例_實用技巧
- 2022-12-29 Python?Base64編碼和解碼操作_python
- 2023-11-16 【云原生】服務器重啟后,如何將dockers和docker里的服務重啟
- 2023-10-17 常用的utlis封裝
- 2022-10-23 C#中數組擴容的幾種方式介紹_C#教程
- 2022-05-11 RabbitMq工作模式深度剖析與Spring整合MQ
- 2021-12-16 jquery+swiper組件實現時間軸滑動年份tab切換效果_jquery
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支