每个生命体的存在,其实实质都是一个简单的过程。很多时候,无需谋求完满的现实状况,毕竟,You are just you。
免责申明
为了防止免费的小哥哥干我,或者呈现其它不好的状况,这里特意注明下:
本文如同题目一样,只属于集体笔记,仅限技术分享~ 如呈现其余状况,一律与自己无关~
本文如同题目一样,只属于集体笔记,仅限技术分享~ 如呈现其余状况,一律与自己无关~
本文如同题目一样,只属于集体笔记,仅限技术分享~ 如呈现其余状况,一律与自己无关~
前言
前段时间,公司忽然来一需要:
- 调研某款 App Android 版微信分享起源动静原理以及实现形式
第一工夫,当然是看看网上有没有前辈开源,借鉴(CV 大法)一波。
查问后果真的是悲喜交加:
- 开森的是,有人钻研过这个货色,也封装好了对应的 SDK。
- 喜剧的是免费,目前已理解的状况最低 100。
对于自身在帝都讨生活的落魄小 Android 而言,无疑是一笔巨款 (手动滑稽~勿喷~)。
都说穷人家的孩子早当家,不得已开始了逆向、剖析之路 ????????????
相干代码已上传 GitHub,当然为了不给本人找事儿,本地命中库就不提供了,本人逆向去拿吧,地址如下:
- https://github.com/HLQ-Strugg...
效果图
空谈无用,来个理论效果图最棒,这里就以我幻想殿堂 App 为例进行测试咯。
筹备工具
基于集体理解简略概述:
- ApkTools: 个别就是为了改包、回包,捎带脚拿个资源文件。
- ClassyShark: 一款贼不便剖析 Apk 工具,个别用于看看大厂都玩啥。
- dex2jar: 将 .dex 文件转换为 .class 文件。
- JD-GUI: 次要是查看反编译后的源代码。
上面附上相干工具网盘链接:
- 链接:https://pan.baidu.com/s/1Ll5c... 明码:20fl
实战开搞
在正式开始前,先来见识下 ClassyShark 这个神器吧。
一、Hi,ClassyShark
首先进入你下载好的 ClassyShark.jar 目录中,随后执行如下命令即可:
- java -jar ClassyShark.jar
示意图如下:
随后在关上的可视化工具中将想看的 Apk 间接拖进去即可:
拖进去之后点击包名,会有一个对以后 Apk 的简略概述:
点击 Methods count 能够查看以后 Apk 办法数:
当然你能够持续往下一层级查看,比方我点击 bilibili:
同样也能够导出文件,这里不作为本文重点论述了,有趣味的能够本人钻研~
二、逆向剖析走起
首先,网上下载指标 App,并将后缀名批改为 zip,随后解压进入该目录:
手动进入已下载实现的 dex-tools-2.1-SNAPSHOT 目录中,执行如下命令:
- sh d2j-dex2jar.sh [指标 dex 文件地址]
例如:
实现之后,将会在 dex-tools-2.1-SNAPSHOT 目录中生成 classes-dex2jar.jar 文件,这里文件就是咱们接下来逆向剖析的靠山呐。
随后将生成的 jar 文件拖入 JD-GUI 中。
查看 AndroidManifest 获取到以后利用包名,有助于咱们一步到位~
因为指标 App 是在文章的详情页中提供分享微信音讯回话以及朋友圈,详情个别集体命名为 XxxDetailsActivity,依据这个思路去搜寻。
有些难堪啊,怎么搜寻到了腾讯的 SDK 呢?
还是手动人工查找吧,????????????
在这块发现个比拟有意思的货色,可能是我比拟 low 吧。一般而言,咱们都晓得混同实体类是必定不能被混同的,不然就会呈现找不到的状况。那么奇怪了,昨天逆向 B 站 Apk,我居然没发现实体类,难道他们的实体类有其余神操作?还是说分包太多我没找到?
终于找到你,文章详情页!!!
操作 App,发现是点击按钮弹出底部分享对话框,原版如下:
随后持续在代码中查看,果然:
这个就很好了解了,自定义一个底部对话框,点击传递分享的 Url 以及分享类型。当初咱们去 ShareArticleDialog 这个类中验证一下猜测是否正确?
看,0 应该是代表分享微信音讯会话,1 代表分享朋友圈。
通过一番排查,发现最终是通过调用如下办法进行分享微信:
public static int send(Context paramContext, String paramString1, String paramString2, String paramString3, Bundle paramBundle) { CURRENT_SHARE_CLIENT = null; if (paramContext == null || paramString1 == null || paramString1.length() == 0 || paramString2 == null || paramString2.length() == 0) { Log.w("MMessageAct", "send fail, invalid arguments"); return -1; } Intent intent = new Intent(); intent.setClassName(paramString1, paramString2); if (paramBundle != null) intent.putExtras(paramBundle); intent.putExtra("_mmessage_sdkVersion", 603979778); int i = getPackageSign(paramContext); if (i == -1) return -1; CURRENT_SHARE_CLIENT = shareClient.get(i); intent.putExtra("_mmessage_appPackage", "这里换成要借壳 App 包名"); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("weixin://sendreq?appid="); stringBuilder.append("这里换成要借壳 AppId"); intent.putExtra("_mmessage_content", stringBuilder.toString()); intent.putExtra("_mmessage_checksum", MMessageUtil.signatures(paramString3, paramContext.getPackageName())); intent.addFlags(268435456).addFlags(134217728); try { paramContext.startActivity(intent); StringBuilder stringBuilder1 = new StringBuilder(); this(); stringBuilder1.append("send mm message, intent="); stringBuilder1.append(intent); Log.d("MMessageAct", stringBuilder1.toString()); return i; } catch (Exception exception) { exception.printStackTrace(); Log.d("MMessageAct", "send fail, target ActivityNotFound"); return -1; } }
在查看微信 SDK 中也发现相似代码,因为掘金这个上传图片宽高我当初还不会调整,临时避免目录地位,感兴趣的小伙伴自行查看:
其它细节就不一一剖析了,间接上代码咯~
三、附上代码~
其实实质借壳分享,集体的了解如下:
- 第一步:绕过微信检测,例如包名、签名是否和微信开放平台绑定统一;
- 第二部:组装参数,间接直击深处,分享微信。
因为此次是 Flutter 我的项目,不得不的面对的是与原生 Android 的交互。因为我是刚刚入坑 Flutter 几周,心田真的是局促不安。
不过值得让人赞叹的是,Flutter 的生态,真的贼棒!尤其我鸡老大,神个别存在!默默的感激我大哥~!
0. 简略聊下 Flutter 与交互
在 Flutter 中文社区中官网对此有这样的一段形容:
Flutter 应用了灵便的零碎,它容许你调用相干平台的 API,无论是 Android 中的 Java 或 Kotlin 代码,还是 iOS 中的 Objective-C 或 Swift 代码。
Flutter 内置的平台特定 API 反对不依赖于任何生成代码,而是灵便的依赖于传递音讯格局。或者,你也能够应用 Pigeon 这个 >
package,通过生成代码来发送结构化类型平安音讯。
- 应用程序中的 Flutter 局部通过平台通道向其宿主(应用程序中的 iOS 或 Android 局部)发送音讯。
- 宿主监听平台通道并接管音讯。而后,它应用原生编程语言来调用任意数量的相干平台 API,并将响应发送回客户端(即应用程序中的 Flutter 局部)。
也就是说,Flutter 充沛给予咱们调用原生 Api 的权力,要害桥梁便是这个通道音讯。
上面一起来看下官网的图:
音讯和响应以异步的模式进行传递,以确保用户界面可能放弃响应。客户端做办法调用的时候 MethodChannel 会负责响应,从平台一侧来讲,Android 零碎上应用 MethodChannelAndroid、 iOS 零碎应用 MethodChanneliOS 来接管和返回来自 MethodChannel 的办法调用。
其实对于我一个老手而言,看这些真的似懂非懂,所以过多的等当前把握了之后再来探讨吧。这块内容将在上面代码局部着重阐明。
1. 引入三方库
api 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+'// 次要用于将分享的在线图片转换为 Bitmapimplementation 'com.github.bumptech.glide:glide:4.11.0'annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'implementation 'com.google.code.gson:gson:2.8.6'
2. 欠缺混同文件
# 爱护我方输入(爱护实体类不被混同)-keep public class com.Your Package Name.bean.**{*;}# Gson-keepattributes Signature# Gson specific classes-keep class sun.misc.Unsafe { *; }-keep class com.google.gson.** { *; }# Application classes that will be serialized/deserialized over Gson-keep class com.google.gson.examples.android.model.** { *; }
3. 编写原生 Android 工具类
这里具体还是须要结合实际我的项目需要而定,不过通用型的一些货色必须要有:
- 动静检测宿主,也能够了解为动静检测借壳指标是否存在;
而剩下的则是分享微信了,这里简略搁置要害代码,详情可点击文章开始的 GitHub 地址。
package com.hlq.struggle.utilsimport android.content.Contextimport android.content.Intentimport android.graphics.Bitmapimport android.os.Bundleimport com.bumptech.glide.Glideimport com.bumptech.glide.load.DataSourceimport com.bumptech.glide.load.engine.GlideExceptionimport com.bumptech.glide.request.RequestListenerimport com.bumptech.glide.request.target.Targetimport com.google.gson.Gsonimport com.google.gson.reflect.TypeTokenimport com.hlq.struggle.app.appInfoJsonimport com.hlq.struggle.bean.AppInfoBeanimport com.tencent.mm.opensdk.modelmsg.SendMessageToWXimport com.tencent.mm.opensdk.modelmsg.WXMediaMessageimport com.tencent.mm.opensdk.modelmsg.WXMediaMessage.IMediaObjectimport com.tencent.mm.opensdk.modelmsg.WXWebpageObjectimport java.io.ByteArrayOutputStreamimport java.io.IOException/** * @author:HLQ_Struggle * @date:2020/6/27 * @desc: */@Suppress("SpellCheckingInspection")class ShareWeChatUtils { companion object { /** * 解析本地缓存 App 信息 */ private fun getLocalAppCache(): ArrayList<AppInfoBean> { return Gson().fromJson( appInfoJson, object : TypeToken<ArrayList<AppInfoBean>>() {}.type ) } /** * 检测用户设施装置 App 信息 */ fun checkAppInstalled(context: Context): Int { var tempCount = -1 // 获取本地宿主 App 信息 val appInfoList = getLocalAppCache() // 获取用户设施已装置 App 信息 val packageManager = context.packageManager val installPackageList = packageManager.getInstalledPackages(0) if (installPackageList.isEmpty()) { return 0 } for (packageInfo in installPackageList) { for (appInfo in appInfoList) { if (packageInfo.packageName == appInfo.packageName) { tempCount++ } } } return tempCount } /** * 命中已装置 App */ private fun hitInstalledApp(context: Context): AppInfoBean? { // 获取本地宿主 App 信息 val appInfoList = getLocalAppCache() // 获取用户设施已装置 App 信息 val packageManager = context.packageManager // 能进入办法阐明本地已存在命中 App,应用时还须要预防 val installPackageList = packageManager.getInstalledPackages(0) for (packageInfo in installPackageList) { for (appInfo in appInfoList) { if (packageInfo.packageName == appInfo.packageName) { return appInfo } } } return null } /** * 分享微信 */ fun shareWeChat( context: Context, shareType: Int, url: String, title: String, text: String, paramString4: String?, umId: String? ) { Glide.with(context).asBitmap().load(paramString4) .listener(object : RequestListener<Bitmap?> { override fun onLoadFailed( param1GlideException: GlideException?, param1Object: Any, param1Target: Target<Bitmap?>, param1Boolean: Boolean ): Boolean { LogUtils.logE(" ---> Load Image Failed") return false } override fun onResourceReady( param1Bitmap: Bitmap?, param1Object: Any, param1Target: Target<Bitmap?>, param1DataSource: DataSource, param1Boolean: Boolean ): Boolean { LogUtils.logE(" ---> Load Image Ready") val i = send( context, shareType, url, title, text, param1Bitmap ) val stringBuilder = StringBuilder() stringBuilder.append("send index: ") stringBuilder.append(i) LogUtils.logE(" ---> Ready stringBuilder.toString() :$stringBuilder") return false } }).preload(200, 200) } private fun send( paramContext: Context, paramInt: Int, paramString1: String, paramString2: String, paramString3: String, paramBitmap: Bitmap? ): Int { val stringBuilder = StringBuilder() stringBuilder.append("share url: ") stringBuilder.append(paramString1) LogUtils.logE(" ---> send :$stringBuilder") val wXWebpageObject = WXWebpageObject() wXWebpageObject.webpageUrl = paramString1 val wXMediaMessage = WXMediaMessage(wXWebpageObject as IMediaObject) wXMediaMessage.title = paramString2 wXMediaMessage.description = paramString3 wXMediaMessage.thumbData = bmpToByteArray( paramContext, Bitmap.createScaledBitmap(paramBitmap!!, 150, 150, true), true ) val req = SendMessageToWX.Req() req.transaction = buildTransaction( "webpage" ) req.message = wXMediaMessage req.scene = paramInt val bundle = Bundle() req.toBundle(bundle) return sendToWx( paramContext, "weixin://sendreq?appid=wxd930ea5d5a258f4f", bundle ) } private fun buildTransaction(paramString: String): String { var paramString: String? = paramString paramString = if (paramString == null) { System.currentTimeMillis().toString() } else { val stringBuilder = StringBuilder() stringBuilder.append(paramString) stringBuilder.append(System.currentTimeMillis()) stringBuilder.toString() } return paramString } private fun bmpToByteArray( paramContext: Context?, paramBitmap: Bitmap, paramBoolean: Boolean ): ByteArray? { val byteArrayOutputStream = ByteArrayOutputStream() try { paramBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) if (paramBoolean) paramBitmap.recycle() val arrayOfByte = byteArrayOutputStream.toByteArray() byteArrayOutputStream.close() return arrayOfByte } catch (iOException: IOException) { iOException.printStackTrace() } return null } private fun sendToWx( paramContext: Context?, paramString: String?, paramBundle: Bundle? ): Int { return send( paramContext, "com.tencent.mm", "com.tencent.mm.plugin.base.stub.WXEntryActivity", paramString, paramBundle ) } private fun send( paramContext: Context?, packageName: String?, className: String?, paramString3: String?, paramBundle: Bundle? ): Int { if (paramContext == null || packageName == null || packageName.isEmpty() || className == null || className.isEmpty()) { LogUtils.logE(" ---> send fail, invalid arguments") return -1 } val appInfoBean = hitInstalledApp(paramContext) val intent = Intent() intent.setClassName(packageName, className) if (paramBundle != null) intent.putExtras(paramBundle) intent.putExtra("_mmessage_sdkVersion", 603979778) intent.putExtra("_mmessage_appPackage", appInfoBean?.packageName) val stringBuilder = StringBuilder() stringBuilder.append("weixin://sendreq?appid=") stringBuilder.append(appInfoBean?.packageSign) intent.putExtra("_mmessage_content", stringBuilder.toString()) intent.putExtra( "_mmessage_checksum", MMessageUtils.signatures(paramString3, paramContext.packageName) ) intent.addFlags(268435456).addFlags(134217728) return try { paramContext.startActivity(intent) val sb = StringBuilder() sb.append("send mm message, intent=") sb.append(intent) LogUtils.logE(" ---> sb :$sb") 0 } catch (exception: Exception) { exception.printStackTrace() LogUtils.logE(" ---> send fail, target ActivityNotFound") -1 } } }}
4. 对 Flutter 裸露通道
这块须要留神几点,当初你能够了解为你在编写一个 Flutter 的小型插件,那么你须要向内部裸露一些你规定的类型,或者说办法。这个不难理解吧。
好比你去调用某个 SDK,官网肯定是告知了一些重要的个性。那么针对咱们当初的这个小插件,它比拟要害的个性又是什么?
对于这个个性,集体这里分为俩个局部来说:
外部个性:
- 本地命中宿主缓存 Json。这块次要是须要集体去保护,去抓去目前罕用的一个 App 的相干信息,不断完善。
内部个性:
- 通道名称。这个了解起来比拟容易,好比你拿着 A 小区的通行证进入 B 小区,那么 B 小区的保安大叔必定会给你拦下来,而反之你进入 A 小区则畅行无阻。
- 对外裸露办法。比如说我当初对外裸露俩个办法,一个为检测命中宿主数量一个为理论的微信分享。
- 要害参数形容。例如微信分享类型,目前偷个懒,Flutter 调用时只须要传递 bool 类型即可,SDK 外部会自行匹配。
针对以上内容,这里提取配置类:
package com.hlq.struggle.app/** * @author:HLQ_Struggle * @date:2020/6/27 * @desc: *//** * 通道名称 */const val channelName = "HLQStruggle"/** * 检测命中数量 > 0 代表可采纳命中宿主计划借壳分享 */const val checkAppInstalledChannel = "checkAppInstalled"/** * 分享微信 */const val shareWeChatChannel = "shareWeChat"/** * 分享微信音讯会话 */const val shareWeChatSession = 0/** * 分享微信朋友圈 */const val shareWeChatLine = 1/** * 本地缓存 App 信息 */const val appInfoJson = "[{\"appName\":\"App Name\",\"downloadUrl\":\"\",\"optional\":1,\"packageName\":\"Package Name\",\"packageSign\":\"App WeChat ID\",\"type\":1}]"
上面则是本地工具类,拼接参数,发送微信:
package com.hlq.struggleimport com.hlq.struggle.app.*import com.hlq.struggle.utils.ShareWeChatUtils.Companion.checkAppInstalledimport com.hlq.struggle.utils.ShareWeChatUtils.Companion.shareWeChatimport io.flutter.embedding.android.FlutterActivityimport io.flutter.embedding.engine.FlutterEngineimport io.flutter.plugin.common.MethodCallimport io.flutter.plugin.common.MethodChannelimport io.flutter.plugins.GeneratedPluginRegistrantclass MainActivity: FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) // 解决 Flutter 传递过去的音讯 handleMethodChannel(flutterEngine) } private fun handleMethodChannel(flutterEngine: FlutterEngine) { MethodChannel(flutterEngine.dartExecutor, channelName).setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result? -> when (methodCall.method) { checkAppInstalledChannel -> { // 获取命中 App 数量 result?.success(checkAppInstalled(activity)) } shareWeChatChannel -> { // 分享微信 val shareType = if (methodCall.argument<Boolean>("isScene")!!) { shareWeChatSession } else { shareWeChatLine } result?.success(shareWeChat( this, shareType, methodCall.argument<String>("shareUrl")!!, methodCall.argument<String>("shareTitle")!!, methodCall.argument<String>("shareDesc")!!, methodCall.argument<String>("shareThumbnail")!!, "")) } else -> { result?.notImplemented() } } } }}
5. Flutter 端调用
这里集体习惯,首先定义一个常量类,将 SDK 或者说 Android 端插件裸露参数定义一下,应用时对立调用,不便而后保护。
/// @date 2020-06-27/// @author HLQ_Struggle/// @desc 常量类/// 通道名称const String channelName = 'HLQStruggle';/// 检测命中数量 > 0 代表可采纳命中宿主计划借壳分享const String checkAppInstalled = 'checkAppInstalled';/// 分享微信const String shareWeChat = 'shareWeChat';
而对于 Flutter 调用 Android 原生则比拟 easy 了,相干留神的点已在代码中正文,这里间接附上对应的要害代码:
class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ GestureDetector( onTap: () async { _shareWeChatApp(true); }, child: Text( '点我分享微信音讯会话', ), ), GestureDetector( onTap: () async { _shareWeChatApp(false); }, child: Padding( padding: EdgeInsets.only(top: 30), child: Text( '点我分享微信朋友圈', ), ), ) ], ), ), ); } /// 具体分享微信形式:true:音讯会话 false:朋友圈 /// 提前调取通道验证采纳官网 SDK 还是借壳计划 void _shareWeChatApp(bool isScene) async { /// 这里肯定留神通道名称俩端统一 const platform = const MethodChannel(channelName); int tempHitNum = 0; try { tempHitNum = await platform.invokeMethod(checkAppInstalled); } catch (e) { print(e); } if (tempHitNum > 0) { // 以后设施存在指标宿主 - 开始执行分享 await platform.invokeMethod(shareWeChat, { 'isScene': isScene, 'shareTitle': '我是分享题目', 'shareDesc': '我是分享内容', 'shareUrl': 'https://juejin.im/post/5eb847e56fb9a0438e239243', /// 分享内容在线地址 'shareThumbnail': 'https://user-gold-cdn.xitu.io/2018/9/27/16618fef8bbf66fb?imageView2/1/w/180/h/180/q/85/format/webp/interlace/1' /// 分享图片在线地址 }); } else { // 以后设施不存在目前宿主 } }}
好了,整个一个流程实现了。咱们看下最初理论分享的成果:
6. 查看成果
- 分享微信音讯会话
分享胜利提醒,重点在分享起源:
分享微信音讯会话,起源胜利变成了我幻想殿堂旗下的某个 App 了。
而分享朋友圈则比较简单了:
番外 - 瞎叨叨
说实话,这个货色不难。
然而磕磕巴巴搞了好几天,也被各种催,甚至差点掏钱去买。
当我很开心的和鸡老大去分享这个事儿整个过程,除了鸡老大日常三连夸之外,老大默默说了个思路,问我是不是这样子的。
默默听完,蛋疼了半天,截然不同!
日常吹鸡老大,老大却淡淡的回复,很失常呀,巴拉巴拉~
老大,不愧是老大~
免责申明
为了防止免费的小哥哥干我,或者呈现其它不好的状况,这里特意注明下:
本文如同题目一样,只属于集体笔记,仅限技术分享~ 如呈现其余状况,一律与自己无关~
本文如同题目一样,只属于集体笔记,仅限技术分享~ 如呈现其余状况,一律与自己无关~
本文如同题目一样,只属于集体笔记,仅限技术分享~ 如呈现其余状况,一律与自己无关~
Thanks
- ClassyShark
- Shrinking Your Build With No Rules and do it with Class(yShark)
- JD-GUI
- dex2jar
- Writing custom platform-specific code:撰写双端平台代码(插件编写实现)
- Flutter Plugin调用Native APIs