開頭
相信大多數互聯網的從業者都有著這樣一個夢想:進大廠,獲得豐厚的薪酬,和更優秀的人一起共事,在技術上獲得更快的成長,
**然而部分人其實一直都陷入了“窮忙”的困局,覺得自己每天白天黑夜都在作業,高強度輸出,但是卻并沒有獲得機會的眷顧,**久而久之,既不知道自己忙什么,也不知道怎么能停下來,
這并不是時間的過錯,而是因為把解決方式過多押注在技術上,然后繼續在作業上不斷回圈,這樣的狀態讓你極度缺少另一個層面的思考,
如何去打破這種僵局呢?很多人建議多讀書,但是從哪種型別的書開始看又該看誰的書呢?說實話,很多技術書寫到最后大同小異,但是萬變不離其宗,源代碼以及參考手冊需要多些鉆研,扎根底層是程式員應有的素養,
現在互聯網訊息如此便捷,學習資料從來不缺,硬碟里都是各種學習資源,上下班坐地鐵,還要刷技術視頻,但是泛看不如精看、精讀,
這里我總結了一些Android核心知識點,以及一些最新的大廠面試題、知識腦圖和視頻資料決議,
需要的**小伙伴私信【學習】**我免費分享給你,以后的路也希望我們能一起走下去,

前言
自己在做SpEditTool:一個支持表情,@mention,#話題#等功能的EditText控制元件,這個專案的時候出現了一個很奇怪的問題
- EditText輸入表情過多的時候,從中間開始洗掉表情,會出現非常卡的情況,而從最后開始洗掉則不會
對比微信的表情輸入功能之后,發現微信這個濃眉大眼的也有這樣的feature(微信都有的現象那能是bug嘛,大霧,,,)
不過自己寫的東西有問題心里總歸不爽,斷斷續續折騰一個禮拜終于把這個問題解決了,整個程序中自己感覺受益匪淺,記錄下分享給大家
最初的實作
setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
return onDeleteEvent();
}
return false;
}
});
private boolean onDeleteEvent() {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
if (selectionEnd != selectionStart) {
return false;
}
SpData[] spDatas = getSpDatas();
for (SpData spData : spDatas) {
if (selectionStart == spData.end) {
Editable editable = getText();
editable.delete(spData.start, spData.end);
return true;
}
}
return false;
}
SpData中保存了表情對應的文本的開始位置和結束位置,直接使用Editable.delete()洗掉
問題定位
粗略定位
先打Log粗略定位下問題,把自己覺得可能會造成卡頓的地方都加了log,發現卡頓的罪魁禍首就是editable.delete(spData.start, spData.end);這一行
精確定位
再準備順藤摸瓜找到卡頓的真正元兇,但是代碼跳著跳著就到SpannableStringBuilder和TextView這兩個超大的類里去了,在哪卡的還不知道自己就繞暈了,只能靠性能檢測工具先具體定位到問題再進一步分析了
這里用到了AndroidStudio3.0自帶的Android Profiler,具體的用法可以看AndroidStudio3.0 Android Profiler分析器
FlameChart
先通過火焰圖看看最耗時的呼叫堆疊是哪一條

圖上可知ChangeWatcher.onSpanChanged()->ChangeWatcher.reflow()->DynamicLayout.reflow()->StaticLayout.generate()這條呼叫堆疊最為耗時
CallChart
再看看呼叫順序圖

- ChangeWatcher.onSpanChanged()被呼叫了多次,會多次呼叫DynamicLayout.reflow()
- DynamicLayout.reflow()中會呼叫多次StaticLayout.generate()
有一點疑問,我看DynamicLayout原始碼,每次reflow()應該只會呼叫一次StaticLayout.generate()而且都是在主執行緒,CallChat卻顯示了多次,而且呼叫次數沒看出啥規律,不知道有沒有大神可以幫我解下惑
BottomUp
其實通過上面兩步基本已經定位到問題了,再在BottomUp的表格中確認一下

StaticLayout.generate()中有這樣一段代碼,這下實錘了
if (spanned == null) {
spanEnd = paraEnd;
int spanLen = spanEnd - spanStart;
measured.addStyleRun(paint, spanLen, fm);
} else {
spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
MetricAffectingSpan.class);
int spanLen = spanEnd - spanStart;
MetricAffectingSpan[] spans =
spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
measured.addStyleRun(paint, spans, spanLen, fm);
}
問題分析
TextView這塊相關代碼比較復雜就不一行行分析了直接說結論
- ChangeWatcher實作了SpanWatcher介面,它是用來監聽TextView中Span發生變化的
- 當從中間洗掉一個表情,被洗掉表情后面的所有的ImageSpan位置都發生了變化,每個ImageSpan變化都會觸發一次
ChangeWatcher.onSpanChanged()->ChangeWatcher.reflow()->DynamicLayout.reflow()->StaticLayout.generate()這樣的呼叫堆疊
這就是為什么要從中間洗掉才會卡頓,從最后刪不會的原因
解決問題
通過以上的結論可以知道,要解決從中間洗掉表情卡頓的關鍵在于如何讓ChangeWatcher.onSpanChanged()不多次呼叫
第一階段方案
之前文章中提到過SpanWatcher繼承于NoCopySpan介面,在產生一個新的Spannable物件時NoCopySpan不會被復制,而ChangeWatcher則實作了SpanWatcher,所以它也不會被復制,靈光一閃一個解決方案出來了
private boolean onDeleteEvent() {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
if (selectionEnd != selectionStart) {
return false;
}
SpData[] spDatas = getSpDatas();
for (int i = 0; i < spDatas.length; i++) {
SpData spData = spDatas[i];
if (selectionStart == spData.end) {
Editable editable = getText();
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(editable);
spannableStringBuilder.delete(spData.start, spData.end);
GifTextUtil.setText(this, spannableStringBuilder);
setSelection(spData.start);
return true;
}
}
return false;
}
- 之前是直接洗掉
- 新的方案是先取出文本內容,復制給新的
SpannableStringBuilder,在設定到輸入框之前洗掉表情,因為此時新的SpannableStringBuilder中并不包含ChangeWatcher所以不會多次呼叫ChangeWatcher.onSpanChanged() - 洗掉表情后再將
SpannableStringBuilder設定給EditText - 最后設定游標位置
完成這一系列操作之后demo一跑,洗掉果然變流暢了,當時心里那個高興啊,竟然做個功能可以比微信實作的還好那么一點
輸入法問題
然而總是帥不過三秒,沒過一會就發現了新的問題,
- 百度輸入法只能一個個洗掉表情,而不能長按一溜刪下來(搜狗是可以的,,,)
剛戰完微信又來個百度輸入法,寫個表情輸入功能咋跟打游戲里的boss一樣呢,本來自信滿滿要找出百度輸入法的bug,但是從來沒接觸過輸入法相關的開發作業,跑了跑google的輸入法的sample還發現官方的輸入法一樣有問題,又掙扎了幾下翻了翻原始碼,最侄訓是無功而返
雖然沒解決輸入法的問題,不過也不是完全沒有識訓
case DO_SEND_KEY_EVENT: {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "sendKeyEvent on inactive InputConnection");
return;
}
ic.sendKeyEvent((KeyEvent)msg.obj);
onUserAction();
return;
}
W/IInputConnectionWrapper: sendKeyEvent on inactive InputConnection連續洗掉時會出現這樣的log,搜狗輸入法也會出現,估計是百度輸入法在出現這樣的情況時就把洗掉按鈕的觸摸事件給中斷了- 出現上面log的原因是因為InputConnection在
setText()時需要被重新創建,而第二次洗掉時InputConnection可能還沒創建好或者IInputConnectionWrapper沒處于激活狀態
完全版的解決方案
跟輸入法死磕幾天未果正愁著呢,突然想到谷歌在android 8.0發布的時候推出了一個Emoji表情庫,Emoji出現在TextView中逃不出也用的是ImageSpan,想看看谷歌會不會也有從中間開始洗掉表情卡頓的feature,就去找了下這個庫的demo,一跑發現demo中不管從末尾還是從中間刪都不會卡,頓時燃起了解決這個問題的希望,看完代碼才發現解決方案如此簡單
之前定位到問題在于ChangeWatcher,但它是一個內部類,自己想的法子都是在外部怎么避免ChangeWatcher.onSpanChanged()被呼叫,谷歌直接簡單粗暴的用反射獲取了ChangeWatcher的Class物件,在setSpan()的時候發現如果是ChangeWatcher就把它包裝在新的WatcherWrapper中,所有的操作都通過WatcherWrapper中轉,就可以隨心所欲控制onSpanChanged了
自定義一個Editable.Factory
- 用反射獲取了
DynamicLayout.ChangeWatcher的Class物件 - 將Class物件作為新的
SpannableStringBuilder的構造引數傳入
final class ImageEditableFactory extends Factory {
private static final Object sInstanceLock = new Object();
@GuardedBy("sInstanceLock")
private static volatile Factory sInstance;
@Nullable
private static Class<?> sWatcherClass;
@SuppressLint({"PrivateApi"})
private ImageEditableFactory() {
try {
String className = "android.text.DynamicLayout$ChangeWatcher";
sWatcherClass = this.getClass().getClassLoader().loadClass(className);
} catch (Throwable var2) {
;
}
}
public static Factory getInstance() {
if (sInstance == null) {
Object var0 = sInstanceLock;
synchronized (sInstanceLock) {
if (sInstance == null) {
sInstance = new ImageEditableFactory();
}
}
}
return sInstance;
}
public Editable newEditable(@NonNull CharSequence source) {
return (Editable) (sWatcherClass != null ? SpannableBuilder.create(sWatcherClass, source)
: super.newEditable(source));
}
}
自定義一個SpannableStringBuilder
- 定義一個WatcherWrapper將ChangeWatcher包裝起來,所有之前對ChangeWatcher的呼叫都通過WatcherWrapper完成
- 這里onSpanChanged就對ImageSpan特殊處理了,直接回傳不呼叫ChangeWatcher.onSpanChanged
- 覆寫SpannableStringBuilder的相關方法
- 對和Span相關的方法特殊處理
貼上WatcherWrapper 的代碼,自定義SpannableStringBuilder代碼就不貼了,大家可以去專案里找com.sunhapper.spedittool.view.SpannableBuilder自己看
private static class WatcherWrapper implements TextWatcher, SpanWatcher {
private final Object mObject;
private final AtomicInteger mBlockCalls = new AtomicInteger(0);
WatcherWrapper(Object object) {
this.mObject = object;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
((TextWatcher) mObject).beforeTextChanged(s, start, count, after);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
((TextWatcher) mObject).onTextChanged(s, start, before, count);
}
@Override
public void afterTextChanged(Editable s) {
((TextWatcher) mObject).afterTextChanged(s);
}
@Override
public void onSpanAdded(Spannable text, Object what, int start, int end) {
if (mBlockCalls.get() > 0 && isImageSpan(what)) {
return;
}
((SpanWatcher) mObject).onSpanAdded(text, what, start, end);
}
@Override
public void onSpanRemoved(Spannable text, Object what, int start, int end) {
if (mBlockCalls.get() > 0 && isImageSpan(what)) {
return;
}
((SpanWatcher) mObject).onSpanRemoved(text, what, start, end);
}
@Override
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart,
int nend) {
if (mBlockCalls.get() > 0 && isImageSpan(what)) {
return;
}
((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend);
}
final void blockCalls() {
mBlockCalls.incrementAndGet();
}
final void unblockCalls() {
mBlockCalls.decrementAndGet();
}
private boolean isImageSpan(final Object span) {
return span instanceof ImageSpan;
}
}
設定EditText的EditableFactory
setEditableFactory(ImageEditableFactory.getInstance());
自己的demo一跑果然無論從哪個位置刪都不會卡頓了
總結
- 性能分析工具可以幫助自己快速定位問題,對于android sdk這種不太好除錯的代碼更是事半功倍
- 解決問題的時候不要一味死磕,特別對于自己不熟悉的東西,有可能思路本身就是錯的
- 對于一些私有的方法,用反射可以實作很多風騷操作~
完整代碼
最后
如果你覺得文章寫得不錯就給個贊唄?如果你覺得那里值得改進的,請給我留言,一定會認真查詢,修正不足,謝謝,
希望讀到這的您能轉發分享和關注一下我,以后還會更新技術干貨,謝謝您的支持!
轉發+點贊+關注,第一時間獲取最新知識點
Android架構師之路很漫長,一起共勉吧!
以下墻裂推薦閱讀!!!
- Android學習筆記參考(敲黑板!!)
- “寒冬未過”,阿里P9架構分享Android必備技術點,讓你offer拿到手軟!
- 畢業3年,我是如何從年薪10W的拖拽工程師成為30W資深Android開發者!
- 騰訊T3大牛帶你了解 2019 Android開發趨勢及必備技術點!
- 八年Android開發,從碼農到架構師分享我的技術成長之路,共勉!
最后祝大家生活愉快~
學習交流
如果你覺得自己學習效率低,缺乏正確的指導,可以加入資源豐富,學習氛圍濃厚的技術圈一起學習交流吧!
群內有許多來自一線的技術大牛,也有在小廠或外包公司奮斗的碼農,我們致力打造一個平等,高質量的Android交流圈子,不一定能短期就讓每個人的技術突飛猛進,但從長遠來說,眼光,格局,長遠發展的方向才是最重要的,
35歲中年危機大多是因為被短期的利益牽著走,過早壓榨掉了價值,如果能一開始就樹立一個正確的長遠的職業規劃,35歲后的你只會比周圍的人更值錢,
如果你覺得自己學習效率低,缺乏正確的指導,可以加入資源豐富,學習氛圍濃厚的技術圈一起學習交流吧!
[外鏈圖片轉存中…(img-N7EE2wGB-1623416741786)]
[外鏈圖片轉存中…(img-RfLYvuXx-1623416741788)]
群內有許多來自一線的技術大牛,也有在小廠或外包公司奮斗的碼農,我們致力打造一個平等,高質量的Android交流圈子,不一定能短期就讓每個人的技術突飛猛進,但從長遠來說,眼光,格局,長遠發展的方向才是最重要的,
35歲中年危機大多是因為被短期的利益牽著走,過早壓榨掉了價值,如果能一開始就樹立一個正確的長遠的職業規劃,35歲后的你只會比周圍的人更值錢,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/287169.html
標籤:其他


