APK 控件逆向工程(36氪,做業盒子)

0. 前言

本文閱讀須要10分鐘. 你可能的收穫:html

  • 學會簡單的反編譯
  • 一些反編譯技巧:實戰提取兩個知名應用的控件,借鑑學習
  • 學會一種提升工做效率的偷懶方法
  • 但願能給讀者正在作的項目有點啓發,少走點彎路

每個android開發程序員頗有必要掌握一些逆向工程的知識技巧,其中好處不勝枚舉,我細數一二:android

1. 反編譯效果展現

1.1 《36氪》下拉刷新(APK下載地址)

各位老鐵能夠下載體驗一下這個app的***下拉刷新***,絕對讓你頗有收穫git

1.1.1原生效果:

36氪下拉刷新.gif

1.1.2《36氪》下拉刷新控件評價
  • 很是流暢,能極大緩解用戶等待數據時的焦灼感. 這點很是牛,市面上的應用多如牛毛,其中不少應用下拉刷新控件的質量卻很堪憂.鄙人覺得《36氪》的下拉刷新體驗在應用市場上絕對名列前茅.他們的UE交互設計師必定花了不少時間精力設計它.
  • 下拉時有縮放效果,等待刷新時填充圖標的動效,整個過程行雲流水
  • 有一種美感在其中.

那麼問題來了,假如你的項目中要實現這樣一個控件,你須要多長時間? 1天? 2天? 個人答案是1個小時 反編譯提取控件,理解其中繪製邏輯.找UI改改樣式,over.程序員

1.1.3反編譯提取控件效果:

36氪-下拉刷新控件提取.gif

嘻嘻,下面我會給個關於反編譯技巧的教程,市面上大多數的應用脫了褲子給咱們看,刺激吧?github

1.2 《做業盒子-教師版》批註控件(APK下載地址)
1.2.1《做業盒子》批註做業效果

做業盒子-批改控件.gif

1.1.3《做業盒子》批註做業控件評價
  • 編輯模式:同時支持雙手縮放,以及單手批註
  • 縮放的同時,批註筆頭粗細跟隨縮放
  • 不可編輯模式:支持雙手縮放,單手拖拽
  • 而且支持批註回退 讓我老老實實作,估計最少也得3天,就算實現了,估計還有很多bug須要修復.沒錯,我就是這麼不自信.有點自知之明仍是好的. 可是,反編譯提取控件,估計也就1,2個小時. 咱們項目也須要實現相似功能.一直有個用戶須要的需求遲遲沒有落地---[圖片上的文字看不清楚,須要支持放大功能,可是同時還得支持批註功能] 由於沒有想到好的交互實現方案,而且有其餘功能要作,作這個需求性價比過低了,開發週期這麼緊,最後不了了之.而如今,簡直是分分鐘的事.
1.2.2《做業盒子》反編譯提取控件

做業盒子-批改控件提取.gif

2. 《36氪》下拉刷新:

2.1正常狀況下,不考慮反編譯,本身實現的思路分析:
  1. 實現跟隨用戶上拉下拉 藍色方框的縮放動畫(使用值動畫)
  2. 刷新過程當中的動畫,若是使用自定義view畫出來,估計成本會比較高,若是使用lottie會省事不少
  3. 不過作出來講不定還有bug,使用反編譯,這樣的擔心會減輕不少,畢竟36kr這個下拉控件通過龐大用戶無數次的考驗.
2.2經過反編譯提取
2.2.1目標:

還原實現下拉刷新控件shell

2.2.2 會遇到的困難:

①控件位置難找; ②資源文件分散; ③代碼通過混淆,代碼邏輯須要跟着做者實現思路走一遍bash

2.2.3 提取過程:
  1. 提取jar包以及資源文件 使用apktool 或使用反編譯集成工具 這一步沒啥難度,建議讀者想跟着實踐一下的話,首選反編譯集成工具. 用命令行工具會很麻煩,光是插件的安裝就這麼多,更別提安裝過程的環境問題.

1.ShakaApkTool 2.Apktool 3.Dex2Jar 4.Zipalign 5.SignApk 6.JDGUI架構

我是直接github上找到一個mac工具軟件:android crack tool app

android crack tool.png
傻瓜化操做後得到以下文件:
image.png
2.隨便新建一個AS項目,將jar包添加到libs 而後add as library 3.在android studio中使用analyse apk
找到項目路徑.png
4.定位下拉刷新控件的代碼位置 這一步須要耐心,由於不太好找,須要猜一下. a,找到mainActivity
mainActivity位置
b.在mainActivity中找到使用該控件的fragment
找到使用該控件的fragment.png

c.關鍵:在fragment中找到控件(須要一點點小耐心) 一開始沒找着,想了一下,這個fragment確定是使用了下拉刷新的,位置沒找錯. 那問題出如今哪裏呢?框架

HomeFragment2中沒有這個控件.png
推測36Kr的程序員對這個下拉刷新動做進行了封裝.可能在BaseFragment中,然而也沒有! 一樣的,在Rxfragment中也沒找着.
BaseFragment沒找到這個控件.png
回到HomeFragment2中,看能不能找到點蛛絲馬跡 果真找到了一點線索:有關於refresh關鍵字
image.png
原來HomeFragment2是另外一個fragment的容器,找錯位置了,回到MainActivity中找其餘fragment 在SubscribeHomeFragment中立刻就找到了
image.png
image.png
image.png

in.srain.cube.views.ptr.PtrFrameLayout //第一反應是網上的開源庫,github上一搜索,果真~
複製代碼

36Kr使用比較出名的下拉刷新庫github地址:android-Ultra-Pull-To-Refresh

d.根據下拉刷新頭部KrHeader以及資源R文件定位資源文件

layout的id.png
佈局文件id.png
根據header_kr這個id去搜索定位佈局文件

e. 根據KrHeader的變量LottieAnimationView b找到lottie動畫 根據lottie文檔官網,動畫文件通常放在asserts文件或res/raw中

image.png

至此,這個控件已經被完徹底全的抽取出來了

image.png

2.2.4 源碼展現

PS:對混淆代碼進行理解後,進行變量以及類名重命名,添加上一些必要註釋

頭部刷新代碼控件()

/**
 * 鄭重聲明:本源碼均來自互聯網,僅供我的欣賞、學習之用,
 * 版權歸36氪產品發行公司全部,任何組織和我的不得公開傳播或用於任何商業盈利用途,
 * 不然一切後果由該組織或我的承擔。
 * 本人不承擔任何法律及連帶責任!請自覺於下載後24小時內刪除
 *
 */
public class KrHeader extends FrameLayout implements PtrUIHandler {
    private ImageView mScaleImageView;
    
    
    private LottieAnimationView mLoadingLottieView;
    private TextView mRefreshInfoTextView;
    
    private boolean isShowRefreshInfo;

    public KrHeader(Context context) {
        this(context, (AttributeSet)null);
    }

    public KrHeader(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        this.init(context);
    }

    public KrHeader(Context context, AttributeSet attributeSet, int defStyleRes) {
        super(context, attributeSet, defStyleRes);
        this.init(context);
    }

    @TargetApi(21)
    public KrHeader(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
        super(context, attributeSet, defStyleAttr, defStyleRes);
        this.init(context);
    }

    private void init() {
        this.mScaleImageView.setVisibility(GONE);
        this.mLoadingLottieView.setVisibility(VISIBLE);
        this.mRefreshInfoTextView.setVisibility(GONE);
        this.mLoadingLottieView.playAnimation();
    }

    private void init(Context var1) {
        View var2 = inflate(var1, R.layout.header_kr, this);
        this.mScaleImageView = (ImageView)var2.findViewById(R.id.pre);
        this.mLoadingLottieView = (LottieAnimationView)var2.findViewById(R.id.loading);
        this.mRefreshInfoTextView = (TextView)var2.findViewById(R.id.tv_refresh_info);
    }

    private void onUIRefreshPrepare() {
        this.mScaleImageView.setVisibility(VISIBLE);
        this.mLoadingLottieView.setVisibility(GONE);
        this.mRefreshInfoTextView.setVisibility(GONE);
        this.mLoadingLottieView.setProgress(0f);
        this.mLoadingLottieView.cancelAnimation();
    }

    private void onUIRefreshComplete() {
        if (this.isShowRefreshInfo) {
            this.mScaleImageView.setVisibility(GONE);
            this.mLoadingLottieView.setVisibility(GONE);
            this.mRefreshInfoTextView.setVisibility(VISIBLE);
        }

    }

    public TextView getCompleteView() {
        return this.mRefreshInfoTextView;
    }

    /**
     * 根據手勢上下拉縮放imageview
     * @param frame
     * @param isUnderTouch
     * @param status
     * @param ptrIndicator
     */
    @Override
    public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
        int offset = frame.getOffsetToRefresh();
        int currentPosY = ptrIndicator.getCurrentPosY();
        if (currentPosY >= offset) {
            this.mScaleImageView.setScaleX(1.0F);
            this.mScaleImageView.setScaleY(1.0F);
        } else if (status == 2) {
            //根據偏移量計算縮放比例
            float scale = (float)(offset - currentPosY) / (float)offset;
            this.mScaleImageView.setScaleX(1.0F - scale);
            this.mScaleImageView.setScaleY(1.0F - scale);
        }

    }

    @Override
    public void onUIRefreshBegin(PtrFrameLayout var1) {
        this.init();
    }

    @Override
    public void onUIRefreshComplete(PtrFrameLayout var1) {
        this.onUIRefreshComplete();
    }

    @Override
    public void onUIRefreshPrepare(PtrFrameLayout var1) {
        this.onUIRefreshPrepare();
    }

    @Override
    public void onUIReset(PtrFrameLayout var1) {
        this.onUIRefreshPrepare();
    }

    public void setShowRefreshInfo(boolean showRefreshInfo) {
        this.isShowRefreshInfo = showRefreshInfo;
    }
}
複製代碼

下拉刷新調用方式

/**
 * 鄭重聲明:本源碼均來自互聯網,僅供我的欣賞、學習之用,
 * 版權歸36氪產品發行公司全部,任何組織和我的不得公開傳播或用於任何商業盈利用途,
 * 不然一切後果由該組織或我的承擔。
 * 本人不承擔任何法律及連帶責任!請自覺於下載後24小時內刪除
 *
 */
public class MainActivity extends AppCompatActivity implements PtrHandler {
    private PtrFrameLayout mPtr;
    private KrHeader krHeader;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mPtr = (PtrFrameLayout) findViewById(R.id.ptr);
        this.krHeader = new KrHeader(this);
        krHeader.setShowRefreshInfo(true);
        krHeader.getCompleteView().setText("暫無更新內容");
        this.mPtr.setHeaderView(this.krHeader);
        this.mPtr.addPtrUIHandler(this.krHeader);
        this.mPtr.setPtrHandler(this);
        this.mPtr.setDurationToCloseHeader(1000);

        this.mPtr.setDurationToClose(200);
        this.mPtr.setLoadingMinTime(1000);
        this.mPtr.setEnabledNextPtrAtOnce(true);
        ImageView iv_test = findViewById(R.id.iv_test);
        iv_test.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, TestActivity.class));
            }
        });

    }

    @Override
    public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
        return true;
    }

    @Override
    public void onRefreshBegin(PtrFrameLayout frame) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mPtr.refreshComplete();

            }
        }, 300);
    }
}
複製代碼
2.2.5 源碼github地址

但願能獲得你的一個star,這是對我寫這篇博文給你們帶來一些另類思路和收穫的鼓勵 36KrRefreshDemo

3. 《做業盒子》批改控件提取

基本上與36KrRefresh相似,關鍵點都在於更多的耐心以及能提取成功的信心 這個控件提取遇到了更多的困難: ①控件涉及多個自定義view,再加上代碼混淆的影響下,才肯定這個控件涉及3個view,提取難度加大很多 ②控件在app中出現的層級更深,定位的時間耗費更多 ③控件中的繪製邏輯更加複雜,須要更多精力去理解混淆後的代碼 PS:過程當中發現了這個控件的一個bug:放大倍數過大,OOM,應用閃退.

源碼地址: DrawContainerDemo 歡迎star,小小鼓勵一下我~

4.授人以魚不如授人以漁

總結一下反編譯參考競品的技巧:

  • 先看主幹,再細看旁枝末節. 什麼意思呢?就是先看其大致項目架構,用了什麼開源庫,瀏覽一下AndroidManifest,都有什麼Activity,經過英文單詞去猜想其功能(一個優秀的項目,對類的命名必然是直觀易懂的)
  • 實踐動手.這個也很重要,由於單純看來的是不許確的,是不可能深刻理解其核心邏輯的,必須儘量的將其抽取出來作成demo,以此驗證本身的猜想.固然,動手是一種冒風險的事情,由於有可能本身的猜想落空.
  • 信心. 內心面的想法:我就有預感本身會成功,蜜汁自信.
  • 耐心. 這事情沒那麼容易,但真沒那麼難. ####5. 反編譯的實用價值(教你偷懶) 不懂偷懶的程序員,不是好程序員.

5.1功利性價值

核心業務的複雜功能實現,可能須要一個月,可是若是你經過反編譯源碼級別地瞭解競品,借鑑競品,說得粗俗點,競品脫了褲子讓你觀摩,那你完成這個功能可能只須要1個星期,節省了三個星期,開發效率提升300%

5.2 自我價值

只是爲了工做敲代碼的程序員,就有點shameless了.不該該只看到其功利性價值,更應該去挖掘自我價值,學習一些優秀程序員敲的商用級別代碼; 這兩個反編譯過程,我是帶着強烈好奇心去完成的:

  • 臥槽,怎麼他們的下拉控件作的這麼好?
  • holy shit,怎麼他們的批註實現思路這麼贊呢? 很是珍惜這種好奇心.好奇心是個好東西,會驅動你去作更多之前沒作過的事,讓你有更多激動人心的發現,會更加想變得優秀,固然,也會讓本身更加開心.

4.28日更新:反編譯註意事項

經老上司彼時芒種提醒,新增了兩個編譯小技巧

  • JD-GUI 能夠查看更完備的源代碼 好比下圖中所示中沒法顯示的源代碼.
    image.png
  • 尋找目標功能或者目標控件源碼時,可經過adb命令查找應用當前界面所對應的activity名稱加快尋找效率
adb shell dumpsys activity | grep "Running activities" -A 7
複製代碼

Running activities.png
最後,感謝你們看完個人文章,以爲不錯的話,點個❤️
相關文章
相關標籤/搜索