乐趣区

Flutter-Notes-|-Android-借壳分享微信

每个生命体的存在,其实实质都是一个简单的过程。很多时候,无需谋求完满的现实状况,毕竟,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:+'
// 次要用于将分享的在线图片转换为 Bitmap
implementation '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.utils

import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.hlq.struggle.app.appInfoJson
import com.hlq.struggle.bean.AppInfoBean
import com.tencent.mm.opensdk.modelmsg.SendMessageToWX
import com.tencent.mm.opensdk.modelmsg.WXMediaMessage
import com.tencent.mm.opensdk.modelmsg.WXMediaMessage.IMediaObject
import com.tencent.mm.opensdk.modelmsg.WXWebpageObject
import java.io.ByteArrayOutputStream
import 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.struggle

import com.hlq.struggle.app.*
import com.hlq.struggle.utils.ShareWeChatUtils.Companion.checkAppInstalled
import com.hlq.struggle.utils.ShareWeChatUtils.Companion.shareWeChat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant

class 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
退出移动版