Android开发

某宝秒杀助手-思路分享

本文主要是介绍某宝秒杀助手-思路分享,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前介

前面以个人角度开源了一篇某宝秒杀插件的源码,今天我给大家讲解下,我构思这款插件的思路。当然若大家不喜欢这种 WG 源码呢?无视即可啦!
相对我个人我来说,我还是比较喜欢玩的!我喜欢从技术中找点乐子。技术本无罪,存在及有价值。让我们从枯燥乏味的代码中以学习的角度找点乐子!

插件介绍

构思

类似这种放开双手的秒杀工具 Andorid 端无疑有以下几种思路:

  1. 分析协议,这种难度相当高,而且没必要为了一个简单的东西搞这么复杂。
  2. 通过寻找应用对应的方法,然后通过 Hook改包 的形式去调用该方法。
  3. 通过模拟按键完成

那我们应该选择哪种方案呢?我当时第一反应选择的是第二种方案,通过寻找某宝的下单方法,然后使用 Xposed 框架 Hook 的形式去调用。
我尝试反编译了某宝,通过 断点Hook 等方案,去寻找某宝下订单的方法。不得不说,某宝的代码量实在是太巨大了,并且增加了反调试逻辑。
最重要的是下订单的流程很长,且每一个步骤都带有重要的参数。因此需要寻找并 Hook 的方法,并不是单纯的 1 个。

确定方案

最终我采用模拟按键来完成秒速下单。关于模拟按键的思路,为大家提供几种方案:

  1. 按键精灵 :它第三方软件,无需会 Andorid 开发,三方提供相应 Api,主要还是通过 adb 命令来实现。
  2. 辅助服务:Andorid 为方便残障人士提供的服务,他可以获取到 AccessibilityNodeInfo 节点,可以向节点发送一些 Actoin 事件。有兴趣大家可以看看,这个东西还可以提高进程优先级哦,能做保活功能。
  3. 通过 Xposed 框架 Hook得到 Activity 的对象,通过 Activity 就能得到对应界面的 View,有了 View 你想怎么模拟怎么模拟啦!

我这里也不卖关子啦!我采用了第 3 种方案,因为相比较 12 方案,不管是 稳定性容错性功能性 都没有第三种方案好。

确定流程

  • 方案确定了,我们来分析下我们要做的事情,首先我们看下 聚划算百亿补贴 活动界面。

  • 当我们确定要抢购的物品后,如这款拖鞋,我们要循环打开这个界面,判断 立即购买 按钮的状态,当可以点击的时候,立刻触发点击事件。

  • 接下来快速选择订单属性,并且点击 确定 按钮。

  • 最后一步点击 提交订单 按钮。这样一系列流程就算完成了,这些过程都要程序自行处理,点击的速度肯定比手要快很多。

开发流程

接下来我们正式开始:

集成Xposed开发框架

这个我觉得没必要说,网上资料很多,大家也可以尝试看看 Xposed 原理。

如何集成 Xposed 插件,请参看本人文章 参考链接

注入控制按钮

要想软件与用户交互,我们必须想办法在界面中注入控制 View ,我们要实通过 Xposed 实现向某宝 DetailActivity 注入 2 个控制按钮。

注入控制按钮,可以通过 3 种方案来实现:

  1. 通过注入悬浮窗,但是需要悬浮窗权限,好处是在整个应用的界面都可以看到。
  2. 通过 ActivitygetWindowManager 方法返回的 WindowManager 调用其 addView 方法(类似 Dialog),这个不需要悬浮窗权限,但是只能在本 Activity 中看到。
  3. 实现效果和方法 2 一样,我们可以强硬一点直接向 ActivityDecorViewcontent 中注入 View

我这里使用了方案 3 ,当然方案 2 也能实现。

注意看 TbDetailActivityHook.ktHookActivityonResume 方法,然后判断 ActivitysimpleName 判断进入的是否是某宝的 DetailActivity,然后向其注入 View

/**
 * 注入抢购按钮
 *
 */
private fun injectView(activity: Activity) {
    // 获取 Activity 的 content View,我们知道它是一个 FrameLayout
    val group = activity.findViewById<View>(android.R.id.content) as ViewGroup
    InjectView(activity).getRootView().let(group::addView)
}
复制代码

接下来看看 InjectView.kt

class InjectView(val activity: Activity) {
    。。。省略
    fun getRootView(): View {
        val item_id = activity.intent.getStringExtra("item_id") ?: ""
        val rootView = activity.UI {
            verticalLayout {
                layoutParams = FrameLayout.LayoutParams(
                    FrameLayout.LayoutParams.WRAP_CONTENT,
                    FrameLayout.LayoutParams.WRAP_CONTENT
                ).apply {
                    gravity = Gravity.RIGHT or Gravity.CENTER
                    setMargins(0, 0, dip(5), 0)
                }

                imageView {
                    imageBitmap = icon_start
                    setOnClickListener {
                        startGo()
                    }
                }.lparams(dip(45), dip(45)) {
                    gravity = Gravity.CENTER
                }

                timeIcon = imageView {
                    imageBitmap = if (JobManagers.haveJob(item_id)) {
                        icon_stop
                    } else {
                        icon_time
                    }

                    setOnClickListener {
                        if (JobManagers.haveJob(item_id)) {
                            JobManagers.removeJob(item_id)
                            imageBitmap = icon_time
                        } else {
                            timeGo {
                                statTimeGo(it, activity)
                                imageBitmap = icon_stop
                            }
                        }
                    }
                }.lparams(dip(45), dip(45)) {
                    setMargins(0, dip(15), 0, 0)
                    gravity = Gravity.CENTER
                }
            }

        }.view

        // 这里记录下 View 方便我下次查找 View
        activity.window.decorView.tag = this
        return rootView
    }
    。。。省略
}
复制代码

主要看 getRootView 这里我使用了 Kotlin 的一个 Anko 库,他可以通过 DSL 语法,快速方便的通过代码布局 UI了解Anko

这里为何要用代码布局 UI,为何不用 xml 构建 UI 呢?这就和 Xposed 的原理相关,由于不再本章范畴就不讲解了,大伙只要知道 Xposed 是不支持资源注入的。

这里有个小技巧,布局可以通过 Anko 框架来构建,若要注入素材呢?比如说注入 2 个图标,这里我提供的方案有 2 种:

  1. 将资源放在网上,然后下载显示,但是需要网络支持,而且比较繁琐。
  2. 将资源 Base64 编码,编码后文本放在代码中,需要的时候解码展示(小图可以这样搞)。

由于我需要的资源不是很多,我这里采用第 2 种方案,至于如何将图片 Base64 编码,可以自己写个小程序或为了方便通过编写了一个 Task 命令完成的。

task buildImage {
    doLast {
        def imageFiles = new File("图片的路径")
        File[] files = imageFiles.listFiles(new FilenameFilter() {

            @Override
            boolean accept(File dir, String name) {
                return name.endsWith(".png")
            }
        })
        for (int i = 0; i < files.length; i++) {
            File file = files[i]
            def bytes = file.readBytes()
            // 进行 Base64 编码
            def result = bytes.encodeBase64().toString()
            println("val " + file.name.replace(".png", "") + "=" + "\"" + result + "\"")
        }

    }
}
复制代码

代码加载 Base64 图片(这里用了一个 lazy 懒加载,小小优化下):

val icon_start by lazy {
    val icon = "Base64 文本"
    val decode = Base64.decode(icon, Base64.NO_WRAP)
    BitmapFactory.decodeByteArray(decode, 0, decode.size)
}
复制代码

刷新打开DetailActivity

为何要频繁打开 DetailActivity 呢?其实原因很简单,如果不到时间 立即购买 的按钮是不可点击的,我们循环启动这个界面,判断 立即购买 按钮是否可以点击,如果可以点了就代表可以抢购了。

我们要循环打开 DetailActivity 这还不简单?我们只需要调用 startActivity 方法就行了呀!思路很简单,这个 DetailActivity 就是我们注入 ViewActivity 。获取 DetailActivityIntent 对象,然后向其 put 一个 Flag 标记,代表这个 DetailActivity 是需要判断抢购的 DetailActivity

/**
 * 开始秒杀 在 Core.kt 中
 */
fun startGo(context: Context, intent: Intent) {
    // 这里最好 clone 一个对象,防止不必要的错误
    val intent = intent.clone() as Intent
    // IS_KILL 就代表是抢购的标记
    intent.putExtra(TbDetailActivityHook.IS_KILL, true)
    // IS_KILL_GO 判断秒杀逻辑直走一次
    intent.putExtra(TbDetailActivityHook.IS_KILL_GO, false)
    // IS_INJECT 这个无需注入控制 View
    intent.putExtra(TbDetailActivityHook.IS_INJECT, false)
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    context.startActivity(intent)
}
复制代码

判断页面加载完毕

我们拿 立即购买 按钮为例,只有当页面的数据加载完毕后,才应该去判断 立即购买 是否可以点击。那页面的数据什么时候加载完毕呢?我一开始卡在这里,但是最后我发现了一个可乘之机。

这里不得不夸奖某宝的技术团队,他们的项目真的是优化到了极致。我发现在加载数据的时候,如果数据没请求下来,关键的 View 对象是不会创建的,应该对 ViewStub 运用的很多吧。
有了这个特点,当 DetailActivity 创建完毕,我们可以开启一个线程,循环去判断 立即购买 按钮是否能找到,如果能找到,就代表数据加载完毕啦!

立即购买 按钮的判断核心代码,在 ClickBuyEvent.kt 中:

object ClickBuyEvent : BaseEvent() {
    。。。。省略
    fun execute(activity: Activity) = mainScope.launch {
        try {
            // 循环判断界面是否加载完毕
            checkLoadCompleteById(
                activity,
                activity.window,
                "ll_bottom_bar_content"
            )

            // 判断是否已经抢完了
            val hintText = judgeSoldThe(activity)

            if (hintText.isBlank()) {
                // 未出售光,点击购买按钮
                clickBuyBtn(activity)
            } else {
                activity.showHintDialog("提示", "检测到物品:$hintText") {}
            }


        } catch (e: Exception) {
            // 出错重新抢购
            Core.startGo(activity.applicationContext, activity.intent.clone() as Intent)
            activity.finish()
        }
    }
    。。。。省略
}
复制代码

注意:我这里用到了 Kotlin 的协程来完成异步操作任务的逻辑。关于 checkLoadCompleteById 就是一个 挂起函数,它的核心功能就是开启线程,循环判断 立即购买 按钮是否能找到(协程真是个好东西,我的抢购任务管理,也是通过它来控制的)。学习协程参考

/**
 * 根据资源 Id  判断资源是否加载完毕
 */
suspend fun checkLoadCompleteById(context: Context, window: Window, nameId: String) {
    withContext(Dispatchers.Default) {
        while (true) {
            // 获取立即购买布局的对应ID
            val resId = context.resources.getIdentifier(
                nameId,
                "id",
                context.packageName
            )

            // 循环判断界面是否加载完毕
            val bottomBarContent: View? = window.findViewById(resId)
            // 若寻找到了  bottomBarContent 代表,界面加载完毕了
            if (bottomBarContent != null) {
                break
            }
        }
    }
}
复制代码

这里有个小技巧!我们知道 findViewById 必须传入一个资源 ID,我们如何获取到这个 ID 呢?
其实也很简单,我们只需要要获取这个 IDname 就行,那这个 name 又怎么查看呢?
我们可以通过 Android Studio 为我们提供的 Layout Inspector 工具(在 Tools -> Layout Inspector ),然后选择某宝进程。

What? 你的手机某宝进程看不到?学习这篇文章你就明白啦!

然后选择 DetailActivity

这样我们就能看到某宝界面的布局信息啦!还还能看 View 的属性方法,我就是通过判断 立即购买 按钮的 isEnabled 属性来判断是否可以抢购的,然后通过 performClick 触发点击事件。

private fun clickBuyBtn(activity: Activity) {
    l("开始执行购买")
    // 进入到这里可以确认界面加载完毕了
    val buy = getBuyButton(activity)
    if (buy != null) {
        l("买: " + buy.text + "  " + buy.isEnabled)
        // 获取到购买按钮,判断是否已经可以开始抢购了
        if (buy.isEnabled) {
            buy.performClick()
        } else {
            // 还没有开始抢购,重新检测
            Core.startGo(activity.applicationContext, activity.intent.clone() as Intent)
            activity.finish()
        }
    } else {
        // 获取按钮失败,重新开始
        Core.startGo(activity.applicationContext, activity.intent.clone() as Intent)
        activity.finish()
    }
}
复制代码

使用技术

  • Xposed 模块开发
  • Kotlin
  • Anko框架
  • Kotlin 协程

总结

最后剩下模拟点击 确定提交按钮 ,其实都和点击 立即购买 一个思路。
大家可以分别看看 OrderChooseEventSubmitOrderEvent 类的实现就可以了。
剩下就是界面交互和简单的业务逻辑,说实话我当时想 3 天完成这个项目的,但是发现越写代码越多,交互起来越麻烦,而且还有一个遗留问题没解决,由于花费时间太长了,等有空在搞吧。

最后:

免责声明

本软件仅供学习使用,完全模拟人工操作,抢购速度取决于你手机的性能与网络,不涉及任何第三方软件接口,本软件已开放源代码,无病毒、不收集用户隐私信息,禁止使用本软件参与非法活动。一切因使用造成的任何后果概不负责,亦不承担任何法律责任!

这篇关于某宝秒杀助手-思路分享的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!