android逆向工程之顺手牵羊(36氪,作业盒子)--超!级!实!用!

0. 前言

本文阅读需要10分钟.
你可能的收获:

  • 学会简单的反编译
  • 一些反编译技巧:实战提取两个知名应用的控件,借鉴学习
  • 学会一种提高工作效率的偷懒方法
  • 希望能给读者正在做的项目有点启发,少走点弯路

每一个android开发程序员很有必要掌握一些逆向工程的知识技巧,其中好处不胜枚举,我细数一二:

1. 反编译效果展示

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

各位老铁可以下载体验一下这个app的下拉刷新,绝对让你很有收获

1.1.1原生效果:
36氪下拉刷新.gif
1.1.2《36氪》下拉刷新控件评价
  • 非常流畅,能极大缓解用户等待数据时的焦灼感. 这点非常牛,市面上的应用多如牛毛,其中很多应用下拉刷新控件的质量却很堪忧.鄙人以为《36氪》的下拉刷新体验在应用市场上绝对名列前茅.他们的UE交互设计师一定花了很多时间精力设计它.
  • 下拉时有缩放效果,等待刷新时填充图标的动效,整个过程行云流水
  • 有一种美感在其中.

那么问题来了,假如你的项目中要实现这样一个控件,你需要多长时间?
1天? 2天?
我的答案是1个小时
反编译提取控件,理解其中绘制逻辑.找UI改改样式,over.

1.1.3反编译提取控件效果:
36氪-下拉刷新控件提取.gif

嘻嘻,下面我会给个关于反编译技巧的教程,市面上大多数的应用脱了裤子给我们看,刺激吧?

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目标:

还原实现下拉刷新控件

2.2.2 会遇到的困难:

①控件位置难找;
②资源文件分散;
③代码经过混淆,代码逻辑需要跟着作者实现思路走一遍

2.2.3 提取过程:
  1. 提取jar包以及资源文件
    使用apktool 或使用反编译集成工具
    这一步没啥难度,建议读者想跟着实践一下的话,首选反编译集成工具.
    用命令行工具会很麻烦,光是插件的安装就这么多,更别提安装过程的环境问题.

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

我是直接github上找到一个mac工具软件:android crack tool
)

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

最后,感谢大家看完我的文章,觉得不错的话,点个❤️

关于素质:

文章分享到《开发者头条》,遇见两个"人而无仪"的人,
这篇文章耗费了我一个星期的下班晚上休息时间,自认为不是什么高深的知识,敢竭鄙诚将自己一些拙浅的想法分享给大家,并没有什么盗用,窃取别人的代码到自己公司实际项目中去用. 遇到这样的人,我也只能用"不患人之不己知"安慰自己.

PS:
本人发文章前已咨询过律师朋友,本文并不构成侵权.如《36氪》《作业盒子》觉得这些混淆过的源码不愿意展示给大家看,可联系本人删除源码.

人而无仪.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,833评论 25 707
  • 我不齿这世上人灵魂的空洞 然而我的灵魂也一样荒芜 从哪里可以寻得 一抹光亮
    Cecilia120阅读 229评论 0 0
  • 喜乐. 眉眼温柔含笑。 唇边悠然带笑。 你行走在被城市规划所遗忘的, 破败又热闹的小路。 来来往往的人, 能带来最...
    酒溪阅读 310评论 0 2
  • 淮南飞花关内还 司机哼曲任休闲 难怪菜总好婆心 寒寒捂脸秀淑颜
    左耳朵冷阅读 174评论 0 0