Android逆向實踐: 使用Smali注入改造YD詞典懸浮窗
前言
最近有個開源APP 咕咚翻譯. 參考我之前在Android無需權限顯示懸浮窗, 兼談逆向分析app中介紹的一個小的細節, 以懸浮窗的形式做了復制查詞功能. 在我寫那篇文章之后, 就一直想有這樣一個能提供復制查詞功能的APP, 無奈自己不知道怎么做一個詞典APP, 也就一直沒管(主要是懶). 自己平時一直用YD詞典, 它也有復制查詞功能, 但是YD懸浮窗的交互我覺得特別蛋疼, 每次安裝還要把懸浮窗權限手動打開才能用.
復制查詞懸浮窗
幾天前下載咕咚翻譯試用, 發現了一個崩潰, 順手改了一下發了pull request. 然后就在想怎么給咕咚翻譯的懸浮窗加上交互, 至少能讓我主動關閉懸浮窗, 參考了iOS上通知的交互方式, 也就是能往下拉一點點, 還能往上滑動關閉, 無奈好像遇到了Android的bug, 就用了一種奇怪的方式實現, 有一定副作用, 于是沒有push到github, 就自己本地用了. 下面故意展示了副作用.
副作用演示
寢室每天都要斷電, 斷電了就沒網, 咕咚翻譯必須聯網查詞, 一到晚上斷電就沒法用. 而YD詞典擁有離線復制查詞功能, 懸浮窗有點蛋疼, 湊合湊合也能用.
需求
我的需求是: 咕咚翻譯能提供離線查詞的功能.
這事說起來簡單, 實際上很復雜, 例如離線詞典數據從哪來? 查詞速度如何? 怎么管理離線詞典數據? 如何實現功能? 沒有找過開源項目, 一直用YD詞典的復制查詞功能, 于是我就盯上了YD詞典.
不知道怎么實現離線查詞, 必然需要研究YD詞典的實現, 在手機上粗略看了一下YD詞典在/data/data下的目錄結構, 大概能確定YD詞典的離線查詞功能實現在native層, 這要我研究到狗年馬月.
今天突然來了個奇思妙想, 既然YD詞典過于龐大, 無法剝離離線查詞功能, 何不將咕咚翻譯的懸浮窗"贈與"YD詞典, 來個移花接木. 之前從來沒有做過這方面的嘗試, 但是憑著自己以往的經驗, 覺得難度不算大, 可以在幾個小時之內搞定.
可行性
想把咕咚翻譯的懸浮窗"贈與"YD詞典, 我只想到了一種方案: Smali注入.
我是個懶人, 一個事情太麻煩我就不想做了, Smali注入這個方案看起來很嚇人, 實際想想可行性非常高.
觀察一下Smali文件的結構:
# class信息
注解信息
實現的接口
static字段
成員字段
直接成員方法
重寫成員方法</code></pre>
Smali文件中對外部類的引用使用類似完全限定名的形式.
可以猜測: 如果一個類只依賴Android framework, 不依賴其他自定義類, 那么直接把這個類的Smali文件放到apktool反編譯生成的目錄中, 不會產生錯誤.
同時, 假設一個類依賴其他自定義類, 如果把整個依賴關系中涉及的所有非Framework的Smali文件都放入apktool生成的目錄中, 同樣不會產生錯誤.
基于以上猜測, 我們可以做到將一個APP中的類安全的添加到另一個APP中.
剩下的就是對被注入APP的Smali代碼進行修改, 使得被注入APP調用注入的Smali代碼, 且能將信息傳遞給注入的Smali代碼.
觀察實現
知道了思路, 就可以實際操作了. 操作前需要了解兩個APP的邏輯, 這樣才能選擇合適的地方進行Smali注入, 減少工作量, 同時減少出錯的可能.
咕咚翻譯的實現
對咕咚翻譯, 我們可以同時觀察它的Java代碼和Smali代碼.
本文粘貼的咕咚翻譯代碼是我修改的版本, 與github上的源碼有區別.
咕咚翻譯中懸浮窗使用MVP的設計, 當View被創建時會使用Dagger 2創建Presenter, 同時進行雙方的依賴注入.
View的實現類TipView關鍵代碼如下所示:
public class TipView extends FrameLayout implements ITipView {
@Inject
protected ITipPresenter mPresenter;
public TipView(Context context) {
this(context,null);
}
public TipView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TipView(Context context, AttributeSet attrs, int defStyleAttr) {
......
DaggerTipComponent.builder().tipModule(new TipModule(this)).build().inject(this);
}
}
Presenter的接口如下所示:
public interface ITipPresenter {
void readyForShow();
void tipShowFully();
void tipHided();
void favoriteClicked(Result result);
void onUserTouch();
void onTouchOver();
}
當TipView
中的收藏按鈕被點擊時, favoriteClicked
方法會被調用. ITipPresenter
的實現類是TipPresenterImpl
我們需要將TipView
的相關類全部注入到YD詞典中, 根據前面的分析, 我們的依賴應當越少越好, 否則一旦類的關系沒設計好, 一個類可能帶起一堆類, 特別麻煩, 所以我把Dagger 2相關的依賴注入改成自己直接手寫, 同時把收藏按鈕去掉, 因為這會導致TipPresenterImpl
中依賴咕咚翻譯Model層, 這會帶起一堆依賴, 為了簡單直接去掉. 懸浮窗的代碼中還使用RxJava相關的東西, 同樣需要把懸浮窗中涉及RxJava的部分去掉.
這一步做完, 我們需要直接注入的類就已經變得很清爽了.
此外, 我們還需要知道如何調用TipView
, 咕咚翻譯里相關代碼如下所示:
public void show(Result result) {
TipView tipView = new TipView(mContext);
tipView.setContent(result);
tipView.setViewManager(mWindowManager);
LayoutParams params = getPopViewParams();
tipView.saveLayoutParams(params);
mWindowManager.addView(tipView, params);
}
在這里我們知道如果要調用TipView
, 我們需要創建它, 同時獲取一個LayoutParams
, 一個Result
, 再通過WindowManager
完成全部調用.
這是我們需要在YD詞典中添加的smali代碼的邏輯, 通過這段代碼來調用我們注入的TipView
.
再觀察上面方法中出現的Result
是什么:
public class Result {
......
public Result(IResult mIResult) {
......
}
......
}
這個類可以通過傳入一個IResult
完成構造, IResult
是個接口, 這其實對我們很有利. 假如YD詞典離線查詞的結果實現了IResult
, 我們就能直接構造一個Result
傳給TipView
了. 因此我們的目的之一就是將YD詞典離線查詞的結果改造成實現IResult
接口. 為此, 我們可以精簡IResult
, 只保留我們需要的方法, 精簡結果如下:
public interface IResult {
List<String> wrapExplains();
String wrapQuery();
int wrapErrorCode();
String wrapPhAm();
}
由于我希望精簡之后, 咕咚翻譯還能正常編譯運行, 所以保留了一個多余的wrapErrorCode
方法, 這個方法在Smali注入中沒用.
總結一下, 在Smali層面上, 我們需要將TipView
的所有代碼注入YD詞典中, 需要在YD詞典中合適的地方編寫調用TipView
的邏輯, 需要讓YD詞典離線查詞結果實現IResult
接口.
YD詞典的實現
對YD詞典, 我們可以觀察的Smali代碼, 也可以觀察比較接近YD詞典源碼的Java代碼.
關于復制查詞的實現, 一個常識是開發者需要在Service
中監聽剪貼板信息變化, 在手機里觀察YD詞典的信息, 可以大概猜是哪個Service
在監聽剪貼板.

ClipboardWatcher
根據名字, 可以猜到在ClipboardWatcher
中, 建議先用jadx或者dex2jar觀察, 畢竟直接看Smali還是太不直觀了, 不到必要的時候不用看Smali.
首先看到了如下代碼:
private class ClipboardListener implements OnPrimaryClipChangedListener {
......
public void onPrimaryClipChanged() {
......
if (ClipboardWatcher.isValidText(clipboardText)) {
......
ClipboardWatcher.this.queryWordViaService(clipboardText);
}
}
}
比較驚喜的是代碼竟然沒有混淆, 這下可以節省不少時間, 不用去猜變量的意思了. 接著看ClipboardWatcher.queryWordViaService
:
private void queryWordViaService(String word) {
if (...) {
QuickQueryService.show(this, word, 0, Util.dip2px(this, BitmapDescriptorFactory.HUE_ORANGE), QuickQueryType.COPY_REQ_POPUP);
}
}
非常簡單, 還是方法調用, 我們接著看QuickQueryService.show
:
public static void show(Context context, String word, int screenX, int screenY, QuickQueryType quickQueryType) {
show(context, word, screenX, screenY, true, true, quickQueryType);
}
private static void show(Context context, String word, int screenX, int screenY, boolean showCloseButton, boolean belowWord, QuickQueryType quickQueryType) {
Intent intent = new Intent(context, QuickQueryService.class);
......
intent.putExtra(WORD, word);
......
context.startService(intent);
}
整個過程很清晰, ClipboardWatcher
負責監聽剪貼板, 當剪貼板內容變化后, 獲取其內容, 交給QuickQueryService
處理. 直接去看QuickQueryService.onStartCommand
, 十有八九是在這里進行下一步邏輯:
public int onStartCommand(Intent intent, int flags, int startId) {
......
try {
if (...) {
......
String word = intent.getStringExtra(WORD);
......
this.handler.obtainMessage(0, Util.deleteRedundantSpace(word)).sendToTarget();
} else if (...) {
......
}
} catch (Exception e) {
e.printStackTrace();
}
return 2;
}
這里不用管其他的邏輯, 只看關鍵代碼, QuickQueryService
從intent
中獲取需要查詢的單詞, 交給handler
處理, 再看handler.handleMessage
的邏輯:
public void handleMessage(Message msg) {
try {
QuickQueryService.this.mainHandler.obtainMessage(0, QueryServerManager.getLocalQueryServer().queryWord(msg.obj)).sendToTarget();
} catch (Exception e) {
e.printStackTrace();
}
}
顯然, handler應該是一個非主線程的handler, 在這里進行了本地查詞相關的工作, 最后把結果交給mainHandler處理, mainHandler.handleMessage
邏輯如下:
public void handleMessage(Message msg) {
try {
QuickQueryService.this.view.setContent(msg.obj);
} catch (Exception e) {
e.printStackTrace();
}
}
可以猜到view
就是YD詞典的懸浮窗類, 服務只需要調用它的setContent
方法, 剩下的由view
自行處理, 這里是復制查詞功能整個調用的終點. 如果我們要進行Smali注入, 這個方法是非常不錯的注入點. 最后一個問題: 上面的msg.obj
是什么?
我們知道這個對象是通過調用QueryServerManager.getLocalQueryServer().queryWord
得到的, 查看這個類的代碼可以很容易知道msg.obj
的類型是YDLocalDictEntity
, 根據之前的討論, 我們需要讓它實現IResult
, 因此我們還要對這個類進行Smali注入.
總結一下, 我們需要對mainHandler.handleMessage
注入代碼, 讓它調用我們的TipView
, 需要對YDLocalDictEntity
注入代碼, 讓它實現IResult
接口. 此外, 由于調用TipView
還需要WindowManager
支持, 因此我們可能還需要對QuickQueryService
進行注入.
實施注入
先用apktool反編譯YD詞典APK. 下面開始進行Smali注入.
直接復制Smali
對于完整的類, 我們不需要手寫Smali代碼, 直接編譯一個咕咚翻譯APK, 再用apktool反編譯, 到對應的路徑下把相應的smali文件復制到YD詞典目錄下.
注意復制smali文件的時候務必要復制全部, 一個java文件可能生成不止一個smali文件, 例如下面是TipView
對應的全部smali文件.

TipView.smali
所有引用到的類的smali都要復制進去, 且按照原APK的包名設置目錄并對應放置.
這一步很簡單, 僅僅是復制一下就完成了.
修改Smali
完成了復制, 還需要添加調用代碼, 注入代碼必須要看smali了.
首先對mainHandler.handleMessage
進行注入.
這里有同學可能會去QuickQueryService.smali里面找代碼, 實際上這部分代碼不在這個文件里, 而是在QuickQueryService$2.smali中, 這主要是因為mainHandler
是一個內部類實例, 內部類實例都是在class$n.smali這種命名的文件里. 要知道具體是哪個文件, 可以看jadx中的初始化代碼, 代碼上都會有注釋寫清楚真正的代碼在哪個文件里, 也可以在QuickQueryService.smali中直接找到答案, 例如QuickQueryService.onCreate
方法中有如下一段:
.method public onCreate()V
......
new-instance v1, Lcom/youdao/dict/services/QuickQueryService$2;
invoke-direct {v1, p0}, Lcom/youdao/dict/services/QuickQueryService$2;-><init>(Lcom/youdao/dict/services/QuickQueryService;)V
iput-object v1, p0, Lcom/youdao/dict/services/QuickQueryService;->mainHandler:Landroid/os/Handler;
......
return-void
.end method
這就是典型的初始化操作, 創建一個實例QuickQueryService$2
, 由v1指向它. 隨后調用v1的<init>
方法, 傳入參數p0, 這個方法完成后對象就構造完畢了, p0就是java中的this
, 之所以內部類能訪問外部類的成員, 一部分原因是因為內部類隱式持有了外部類的引用, 這個引用就是在這里被傳入的. 最后v1的值存入了p0的成員mainHandler
中. 換句話說, 這三句就是初始化mainHandler
用的, 可知mainHandler
的代碼在QuickQueryService$2.smali
中. 直接到QuickQueryService$2.smali
中找handleMessage
方法, 代碼如下(可以略過這段smali代碼):
# virtual methods
.method public handleMessage(Landroid/os/Message;)V
.locals 3
.param p1, "msg" # Landroid/os/Message;
.prologue
.line 85
:try_start_0
iget-object v1, p1, Landroid/os/Message;->obj:Ljava/lang/Object;
check-cast v1, Lcom/youdao/dict/model/YDLocalDictEntity;
.line 86
.local v1, "entity":Lcom/youdao/dict/model/YDLocalDictEntity;
iget-object v2, p0, Lcom/youdao/dict/services/QuickQueryService$2;->this$0:Lcom/youdao/dict/services/QuickQueryService;
# getter for: Lcom/youdao/dict/services/QuickQueryService;->view:Lcom/youdao/dict/widget/QuickQueryView;
invoke-static {v2}, Lcom/youdao/dict/services/QuickQueryService;->access$100(Lcom/youdao/dict/services/QuickQueryService;)Lcom/youdao/dict/widget/QuickQueryView;
move-result-object v2
invoke-virtual {v2, v1}, Lcom/youdao/dict/widget/QuickQueryView;->setContent(Lcom/youdao/dict/model/YDLocalDictEntity;)V
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
.line 90
.end local v1 # "entity":Lcom/youdao/dict/model/YDLocalDictEntity;
:goto_0
return-void
.line 87
:catch_0
move-exception v0
.line 88
.local v0, "e":Ljava/lang/Exception;
invoke-virtual {v0}, Ljava/lang/Exception;->printStackTrace()V
goto :goto_0
.end method
別看這段代碼這么長, 實際上就是下面這段Java代碼:
public void handleMessage(Message msg) {
try {
QuickQueryService.this.view.setContent(msg.obj);
} catch (Exception e) {
e.printStackTrace();
}
}
因為內部類訪問外部類實例的本質, 是通過編譯器給咱們加的各種合成方法(Synthetic Method)實現的, 所以轉換成smali之后特別冗長.
我們要做的就是把這段smali代碼改成類似下面的Java代碼的效果
public void handleMessage(Message msg) {
TipView tipView = new TipView(mContext);
Result result = new Result((YDLocalDictEntity) msg.obj);
tipView.setContent(result);
tipView.setViewManager(QuickQueryService.this.mWindowManager);
LayoutParams params = QuickQueryService.getPopViewParams();
tipView.saveLayoutParams(params);
QuickQueryService.this.mWindowManager.addView(tipView, params);
}
但是這段代碼的能跑的前提是YDLocalDictEntity
實現了IResult
, 以及QuickQueryService
有一個成員變量mWindowManager
和一個靜態方法getPopViewParams
.
YDLocalDictEntity注入
打開YDLocalDictEntity.smali, 和寫Java一樣, 一個類要實現一個接口, 需要寫implements interface_name
, 同時實現方法, smali也類似, 修改后smali如下所示, 添加的代碼我重點標出來了:
.class public Lcom/youdao/dict/model/YDLocalDictEntity;
.super Ljava/lang/Object;
.source "YDLocalDictEntity.java"
# interfaces
.implements Ljava/io/Serializable;
#========在這里添加要實現的接口==================
.implements Lname/gudong/translate/mvp/model/entity/IResult;
#=============================================
......
#======下面是IResult四個方法的實現=============
.method public wrapQuery()Ljava/lang/String;
.locals 1
.prologue
iget-object v0, p0, Lcom/youdao/dict/model/YDLocalDictEntity;->word:Ljava/lang/String;
return-object v0
.end method
.method public wrapExplains()Ljava/util/List;
.locals 1
.annotation system Ldalvik/annotation/Signature;
value = {
"()",
"Ljava/util/List",
"<",
"Ljava/lang/String;",
">;"
}
.end annotation
.prologue
iget-object v0, p0, Lcom/youdao/dict/model/YDLocalDictEntity;->translations:Ljava/util/ArrayList;
return-object v0
.end method
.method public wrapErrorCode()I
.locals 1
.prologue
const v0, 0x0
return v0
.end method
.method public wrapPhAm()Ljava/lang/String;
.locals 1
.prologue
iget-object v0, p0, Lcom/youdao/dict/model/YDLocalDictEntity;->phoneticUS:Ljava/lang/String;
return-object v0
.end method
#=============================================
iget-object v0, p0, field
可以理解為v0 = p0.field
.
這里wrapQuery
, wrapExplains
和wrapPhAm
三個方法都只是取當前對象中的一個成員返回, wrapErrorCode
純粹是為了兼容才寫入接口的, 直接返回0. 這些代碼可以從類似的Java代碼對應的smali中修改得到, 也可以直接寫, 畢竟這些代碼很簡單.
QuickQueryService注入
我們需要給QuickQueryService添加一個WindowManager
成員和一個靜態方法, 為了方便代碼書寫, 全部使用public修飾.
QuickQueryService.smali修改后如下:
......
.field private view:Lcom/youdao/dict/widget/QuickQueryView;
#============添加一個成員 mWindowManager==============
.field public mWindowManager:Landroid/view/WindowManager;
#==================================================
......
.method public onCreate()V
.locals 3
......
#=======初始化 mWindowManager==================
const-string/jumbo v0, "window"
invoke-virtual {p0, v0}, Landroid/content/Context;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
move-result-object v0
check-cast v0, Landroid/view/WindowManager;
iput-object v0, p0, Lcom/youdao/dict/services/QuickQueryService;->mWindowManager:Landroid/view/WindowManager;
#=======================================
.line 108
return-void
.end method
......
#====添加靜態方法 getPopViewParams==========
.method public static getPopViewParams()Landroid/view/WindowManager$LayoutParams;
.locals 8
...(建議用Java寫了反編譯復制過來)...
.end method
#=====================
添加成員可以說是依葫蘆畫瓢, 初始化也很容易寫, 注意把初始化的代碼放到onCreate
方法的最下面, 因為這個方法無返回值, 因此在方法末尾可以隨意使用寄存器, 不需要操心破壞寄存器里的原始值. 靜態方法的聲明很容易, 但是這個方法代碼量大, 建議用Java寫了反編譯了復制, 這里就不貼了, 實在太長了, 光看到.locals 8
就夠嚇人了.
mainHandler注入
我們只需要注入mainHandler.handleMessage
, 但是因為代碼較多, 需要仔細寫, 這里我們沒有動外層的try-catch, 直接在內層做修改, 注釋標明了這塊區域:
# virtual methods
.method public handleMessage(Landroid/os/Message;)V
.locals 3
.param p1, "msg" # Landroid/os/Message;
.prologue
.line 85
:try_start_0
iget-object v1, p1, Landroid/os/Message;->obj:Ljava/lang/Object;
check-cast v1, Lcom/youdao/dict/model/YDLocalDictEntity;
.line 86
.local v1, "entity":Lcom/youdao/dict/model/YDLocalDictEntity;
iget-object v2, p0, Lcom/youdao/dict/services/QuickQueryService$2;->this$0:Lcom/youdao/dict/services/QuickQueryService;
#前面的代碼使得v1是YDLocalDictEntity, v2是QuickQueryService
#=======下面是注入代碼=============
#v0指向一個Result, 使用v1做參數初始化, v1是YDLocalDictEntity
new-instance v0, Lname/gudong/translate/mvp/model/entity/Result;
invoke-direct {v0, v1}, Lname/gudong/translate/mvp/model/entity/Result;-><init>(Lname/gudong/translate/mvp/model/entity/IResult;)V
#將v1改為指向一個TipView, 使用v2做采納數初始化, v2是QuickQueryService
new-instance v1, Lname/gudong/translate/listener/view/TipView;
invoke-direct {v1, v2}, Lname/gudong/translate/listener/view/TipView;-><init>(Landroid/content/Context;)V
#下面這句等于v1.setContent(v0)
invoke-virtual {v1, v0}, Lname/gudong/translate/listener/view/TipView;->setContent(Lname/gudong/translate/mvp/model/entity/Result;)V
#下面這句將v0指向QuickQueryService.this.mWindowManager
iget-object v0, v2, Lcom/youdao/dict/services/QuickQueryService;->mWindowManager:Landroid/view/WindowManager;
#等于v1.setViewManager(v0)
invoke-virtual {v1, v0}, Lname/gudong/translate/listener/view/TipView;->setViewManager(Landroid/view/ViewManager;)V
invoke-static {}, Lcom/youdao/dict/services/QuickQueryService;->getPopViewParams()Landroid/view/WindowManager$LayoutParams;
#下面這句將上面方法得到的結果存到v0, 也就是說v0此時是LayoutParams
move-result-object v0
#v1.saveLayoutParams(v0)
invoke-virtual {v1, v0}, Lname/gudong/translate/listener/view/TipView;->saveLayoutParams(Landroid/view/WindowManager$LayoutParams;)V
# v2是QuickQueryService, 下面這句等于v2 = v2.mWindowManager, 此時v2是mWindowManager
iget-object v2, v2, Lcom/youdao/dict/services/QuickQueryService;->mWindowManager:Landroid/view/WindowManager;
#等于v2.addView(v1, v0)
invoke-interface {v2, v1, v0}, Landroid/view/WindowManager;->addView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V
#======上面是注入代碼========
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
.line 90
.end local v1 # "entity":Lcom/youdao/dict/model/YDLocalDictEntity;
:goto_0
return-void
.line 87
:catch_0
move-exception v0
.line 88
.local v0, "e":Ljava/lang/Exception;
invoke-virtual {v0}, Ljava/lang/Exception;->printStackTrace()V
goto :goto_0
.end method
如果不明白注入的代碼, 可以看我寫的注釋, 總體上來看還是很簡單的. 當然, 我手寫的代碼的效率沒有生成的高.
添加資源
由于TipView
中會使用layout, 所以還需要把layout下的文件放到YD詞典目錄的對應位置. 而且需要自己在res/values/ids.xml和res/values/public.xml中添加一些內容. 如果layout中引用了drawable, 還需要把對應的drawable放到YD詞典目錄的對應位置. 引用了color, dimen等的, 都需要添加對應的定義.
apktool在打包的時候會用aapt來幫助生成id, 但是實際上smali文件中已經沒有對R文件的引用了, 全是常量, 所以對于代碼中直接使用的R.id.name
, 需要我們自己到ids.xml中添加id, 然后到public.xml中指定好唯一的值, 再把smali中的常量替換成我們定義的, 對于layout, 只需要到public.xml中指定好值, 把smali中R.layout.name
換成我們指定的值就行. 其他的如color, dimen的, aapt會自動幫我們生成id. 但如果直接在代碼中使用了, 還是要和layout一樣, 自己去定義.
例如我在ids.xml中添加了如下內容:
<item type="id" name="pop_view_content_all">false</item>
<item type="id" name="pop_view_content_without_shadow">false</item>
<item type="id" name="ll_pop_src">false</item>
<item type="id" name="tv_pop_src">false</item>
<item type="id" name="tv_pop_phonetic">false</item>
<item type="id" name="ll_pop_dst">false</item>
<item type="id" name="tv_point">false</item>
在public.xml中添加了如下內容:
<public type="layout" name="pop_view" id="0x7f0301a7" />
<public type="id" name="pop_view_content_all" id="0x7f0d0629" />
<public type="id" name="pop_view_content_without_shadow" id="0x7f0d062a" />
<public type="id" name="ll_pop_src" id="0x7f0d062b" />
<public type="id" name="tv_pop_src" id="0x7f0d062c" />
<public type="id" name="tv_pop_phonetic" id="0x7f0d062d" />
<public type="id" name="ll_pop_dst" id="0x7f0d062e" />
<public type="id" name="tv_point" id="0x7f0d062f" />
簽名與安裝
最后使用apktool打包, 使用jarsigner簽名, 就可以安裝到手機上了, YD詞典的功能均可用, 同時懸浮窗被替換成了咕咚翻譯的懸浮窗.

yd_with_gd
尾聲
這個YD詞典給我的印象一直是卡卡的, 用著還行, 這次逆向順便把它的硬件加速開了, 流暢很多, 也不知道這個APP還有沒有人維護, 怎么連硬件加速都不愿意開. 用Smali注入給它換個懸浮窗本來只是一個想法, 感覺這個想法挺有意思的, 就試了一下, 花了8小時才做出來, 現在手機復制查詞爽多了.
文/Shawon(簡書作者)
來源:http://www.jianshu.com/p/6e5082b9d2e2