每个生命体的存在,其实实质都是一个简单的过程。很多时候,无需谋求完满的现实状况,毕竟,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