前面以个人角度开源了一篇某宝秒杀插件的源码,今天我给大家讲解下,我构思这款插件的思路。当然若大家不喜欢这种 WG
源码呢?无视即可啦!
相对我个人我来说,我还是比较喜欢玩的!我喜欢从技术中找点乐子。技术本无罪,存在及有价值。让我们从枯燥乏味的代码中以学习的角度找点乐子!
类似这种放开双手的秒杀工具 Andorid
端无疑有以下几种思路:
Hook
或 改包
的形式去调用该方法。那我们应该选择哪种方案呢?我当时第一反应选择的是第二种方案,通过寻找某宝的下单方法,然后使用 Xposed
框架 Hook
的形式去调用。
我尝试反编译了某宝,通过 断点
丶 Hook
等方案,去寻找某宝下订单的方法。不得不说,某宝的代码量实在是太巨大了,并且增加了反调试逻辑。
最重要的是下订单的流程很长,且每一个步骤都带有重要的参数。因此需要寻找并 Hook
的方法,并不是单纯的 1
个。
最终我采用模拟按键来完成秒速下单。关于模拟按键的思路,为大家提供几种方案:
Andorid
开发,三方提供相应 Api
,主要还是通过 adb
命令来实现。Andorid
为方便残障人士提供的服务,他可以获取到 AccessibilityNodeInfo
节点,可以向节点发送一些 Actoin
事件。有兴趣大家可以看看,这个东西还可以提高进程优先级哦,能做保活功能。Xposed
框架 Hook
得到 Activity
的对象,通过 Activity
就能得到对应界面的 View
,有了 View
你想怎么模拟怎么模拟啦!我这里也不卖关子啦!我采用了第 3
种方案,因为相比较 1
和 2
方案,不管是 稳定性
丶 容错性
丶 功能性
都没有第三种方案好。
聚划算百亿补贴
活动界面。拖鞋
,我们要循环打开这个界面,判断 立即购买
按钮的状态,当可以点击的时候,立刻触发点击事件。确定
按钮。提交订单
按钮。这样一系列流程就算完成了,这些过程都要程序自行处理,点击的速度肯定比手要快很多。接下来我们正式开始:
这个我觉得没必要说,网上资料很多,大家也可以尝试看看 Xposed
原理。
如何集成 Xposed 插件,请参看本人文章 参考链接
要想软件与用户交互,我们必须想办法在界面中注入控制 View
,我们要实通过 Xposed
实现向某宝 DetailActivity
注入 2
个控制按钮。
注入控制按钮,可以通过 3
种方案来实现:
Activity
的 getWindowManager
方法返回的 WindowManager
调用其 addView
方法(类似 Dialog
),这个不需要悬浮窗权限,但是只能在本 Activity
中看到。2
一样,我们可以强硬一点直接向 Activity
的 DecorView
的 content
中注入 View
。我这里使用了方案
3
,当然方案2
也能实现。
注意看 TbDetailActivityHook.kt
中 Hook
了 Activity
的 onResume
方法,然后判断 Activity
的 simpleName
判断进入的是否是某宝的 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
种:
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
这还不简单?我们只需要调用 startActivity
方法就行了呀!思路很简单,这个 DetailActivity
就是我们注入 View
的 Activity
。获取 DetailActivity
的 Intent
对象,然后向其 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
呢?
其实也很简单,我们只需要要获取这个 ID
的 name
就行,那这个 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
协程最后剩下模拟点击 确定
和 提交按钮
,其实都和点击 立即购买
一个思路。
大家可以分别看看 OrderChooseEvent
与 SubmitOrderEvent
类的实现就可以了。
剩下就是界面交互和简单的业务逻辑,说实话我当时想 3
天完成这个项目的,但是发现越写代码越多,交互起来越麻烦,而且还有一个遗留问题没解决,由于花费时间太长了,等有空在搞吧。
最后:
本软件仅供学习使用,完全模拟人工操作,抢购速度取决于你手机的性能与网络,不涉及任何第三方软件接口,本软件已开放源代码,无病毒、不收集用户隐私信息,禁止使用本软件参与非法活动。一切因使用造成的任何后果概不负责,亦不承担任何法律责任!