APP端 用户来源追踪接入友盟SDK实现参数传递

757 阅读1分钟

一、背景

解决 APP 端的用户来源追踪的需求, 目标能统计用户通过哪些其他渠道实现的下载,当用户点击下载链接,可以将链接上的参数传递到 APP 内部的 h5 上,用于后续的开发流程, 所以就需要用户无论是引导到下载打开app还是直接通过唤起 app 都可以传入参数

二、一些用户来源追踪的方式比较

2-1、ios 官方推荐活动统计

官方文档说明

image.png 优点:

  • 相对来说最真实

缺点:

  • 只有官方报表,但有延迟,而且目前看只有下载量统计
  • 需要至少五个以上下载才有报表
  • 跟业务无法对接,无法满足自定义传参的目的
2-2、android 端

引入官方的Play Install Referrer SDK可在代码中获取渠道

https://play.google.com/store/apps/details?id=com.kalodata.kalodata_android&referrer=${推荐来源参数}

可以在下载地址上通过参数传递

优点:

  • 感觉基本满足需求,可以自定义统计下载,注册等能力
  • 比较精确的能统计用户的来源

缺点:

  • 也是不太方便使用,无法判断当前用户是否是下载还是直接打开app,不太通用,场景可能仅限于下载打开链路的统计
2-3、自己实现

流程图

image.png

scheme url

在app端是可以通过scheme url 传参的方式在用户打开app时候接收到参数的 配置类似 app:// 这种协议

实现scheme url 只需要在配置中设置url types

image.png

在safari 中输入 myapp://xxxx 即可唤起

接收scheme url 打开app的方法解析链接

AppDelegate.m 中

#pragma mark -- openURL options --
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options{
    return YES;
};

在 iOS 13 及更高版本中,Apple 引入了 Scene Delegate 来管理应用的界面和生命周期,特别是在支持多窗口的设备上,如 iPad。这意味着对于支持多任务和多窗口的应用,处理 URL Scheme 的方式也有所变化

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
    for (UIOpenURLContext *context in URLContexts) {
        NSURL *url = context.URL;
        // 检查 URL 是否是你应用的 scheme
        if ([url.scheme isEqualToString:@"myapp"]) {
            // 处理 URL,例如打开特定的视图控制器或传递数据
            [self handleCustomSchemeURL:url];
        }
    }
}
Universal Links

可以通过配置网站链接的方式唤起app, 需要在域名对应服务器后台添加 apple-app-site-association 文件, 并在项目中配置 Associated Domains

image.png

测试网页,可以在这个网站测试app的跳转及传参 www.debug-anywhere.com/cn/tools/ur…

如果你的应用不支持多任务(iOS 13 之前的项目),你可以在 AppDelegate 中处理 Universal Links: AppDelegate.m

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        NSURL *url = userActivity.webpageURL;
        // 处理 URL
        [self handleUniversalLink:url];
    }
    return YES;
}

- (void)handleUniversalLink:(NSURL *)url {
    // 在这里处理你的 Universal Link
    if ([url.host isEqualToString:@"yourdomain"]) {
        // 执行相关操作,比如打开特定的视图控制器
    }
}

ios 13之后

// 处理深度链接 Universal link的回调
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        NSURL *url = userActivity.webpageURL;
        // 处理 URL
        [self handleUniversalLink:url];
    }
}

- (void)handleUniversalLink:(NSURL *)url {
    // 在这里处理你的 Universal Link
    if ([url.host isEqualToString:@"yourdomain"]) {
        // 执行相关操作,比如打开特定的视图控制器
    }
}

优点:

  • 方便实现,灵活传参

缺点:

  • 没有后台查看统计数据
  • 无法实现用户下载后直接打开时的参数传入,安卓可以实现,ios 官方没有提供这种方式
  • 安卓和ios各有不同的实现方式,
2-4、使用第三方的方式实现
1、使用 openInstall

文档 www.openinstall.io/doc/ios_sdk…

2、使用 xInstall

文档 www.xinstall.com.cn/

以上两种是搜索时软文最多的,实现的功能和方式原理都比较类似, 核心卖点是用户首次安装唤起时传参的问题, 背后原理是使用ip模糊匹配的方式实现, 当用户在h5端点击链接时,会将当前设备的ip、设备信息、等上传到自己服务器,当用户首次下载好app时,在匹配上传的内容,将参数取到,基本使用费用在一年七八千左右

3、使用友盟 ULink

后面发现友盟 Ulink 也可以实现一样的需求,而且友盟在app端的业务感觉更专业一些, 最重要的是 Ulink 在友盟上是免费的 , 所以后面将介绍如何在ios 和 android 上接入 友盟 SDK

文档: developer.umeng.com/docs/191212…

优点

  • 免费
  • 可以灵活传参
  • 自动给生成通用链接,目的是ios上跨域才能唤起app,如果当前域名是有效的h5页面,点击会打开浏览器页面,无法唤起app
  • 不仅有模糊匹配还有剪切板提高匹配成功率

三、接入友盟 SDK

3-1、 后台配置

在友盟后台新建ios和android应用 image.png

在这里配置好 schemeUrl app 无法打开时的下载地址, 配置 universal link 开启剪切板能力

3-2、h5 端代码的实现

引入文件

  <script src="https://g.alicdn.com/jssdk/u-link/index.min.js"></script>

集成功能

import { LOCALSTORAGE_KEY } from "@/config";
import storage from "@/utils/storage";
import { useEffect, useState } from "react";

const useULink = () => {
  const tc = storage.getItem(LOCALSTORAGE_KEY.TRACK_CODE);
  const [clipboardText, setclipboardText] = useState("");
  useEffect(() => {
    if (window.ULink) {
      window.ULink({
        id: "用户渠道里生成的linkid",
        data: {
          tc,
        },
        selector: "#Go_TO_APP",
        timeout: 2000,
        useOpenInBrowerTips: "default",
        lazy: false,
        auto: false,
        useClipboard: false,
        proxyOpenDownload: myDownloadStyle,
        onready: function (ctx) {
          var u = navigator.userAgent;
          var isAndroid = u.indexOf("Android") > -1 || u.indexOf("Adr") > -1;
          // 这里对安卓端特殊处理了,因为安卓端在发布google play审核无法通过,改成了复制参数,并在安卓端粘贴参数的方案实现
          if (isAndroid) {
            if (storage.getItem(LOCALSTORAGE_KEY.TRACK_CODE)) {
              return setclipboardText("tc=" + storage.getItem(LOCALSTORAGE_KEY.TRACK_CODE));
            }
          }
          if (ctx.solution?.clipboardToken) {
            setclipboardText(ctx.solution.clipboardToken);
          }
        },
      });
    }
  }, []);
  function myDownloadStyle(defaultAction, LinkInstance) {
    // 解决安卓未安装,无法下载调转问题
    if (LinkInstance.solution.downloadUrl.includes("android")) {
      window.location.href = LinkInstance.solution.downloadUrl;
    }
    return;
    // 可以自定义打开失败的弹窗,这里设置不展示弹窗了
    const downloadStyle = true;
    if (downloadStyle === true) {
      const downloadStyleDom = document.createElement("div");
      downloadStyleDom.setAttribute("id", "downloadStyle");

      downloadStyleDom.innerHTML = `
        <div id="download-window" style="
          width: 70%;
          height: 130px;
          padding: 20px;
          background-color: #fff;
          border: 1px solid #ccc;
          position: absolute;
          top: 30%;
          left: 10%">
        <div onclick="window.ulinkCloseDownloadTip()" style="
          position: absolute;
          top: 4px;
          right: 10px;
        ">X</div>
        <p>请点击下方下载按钮跳转至商店进行下载!</p>
        <button style="
           width: 70%;
           height: 40px;
           background-color: #3b82fe;
           color: #fff;
           border-radius: 20px;
           border: none;
           margin: 0 auto;
           display: block;
         " onclick="window.ulinkOpenDownload()">立即下载</button>
         </div>
                  `;

      window.ulinkCloseDownloadTip = function () {
        document.getElementById("download-window").remove();
      };
      window.ulinkOpenDownload = function () {
        window.location.href = LinkInstance.solution.downloadUrl;
      };
      document.body.appendChild(downloadStyleDom);
    } else {
      defaultAction();
    }
  }

  return {
    clipboardText,
    tc,
  };
};

export default useULink;

当用户点击带有id 为 Go_TO_APP 的标签时,触发功能, 也可以通过配置改为首次加载即触发

<div
  id="Go_TO_APP"
  className="btn btn_primary"
  onClick={() => {
        Taro.setClipboardData({
          data: clipboardText || "",
          success() {},
        });
  }}
>
  <Trans>Go to the Kalodata APP</Trans>
</div>
3-3、ios 端代码的实现
1、通过 Pods 安装三个库 集成文档

image.png

2、初始化SDK, 处理首次加载获取参数的逻辑
#import <UMLink/UMLink.h>
#import <UMCommon/UMConfigure.h>

// 获取首次安装时参数
- (void)installGetParams{
  // 这里做了一个延时处理, 正常ios上调用这个sdk需要合规用户必须点击授权同意才可以调用,通过延时2s可以绕过这个限制, 没有延时该sdk初始化后立马请求也会报错
  
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      [UMConfigure initWithAppkey:@"xxxxxxxxxxxxx" channel:@"App Store"];
      BOOL hasGetInstallParams = [[NSUserDefaults standardUserDefaults] boolForKey:@"key_Has_Get_InstallParams"];
      // 通过参数控制只在安装时调用一次
      if (!hasGetInstallParams) {
          // 获取安装参数的方法
          [MobClickLink getInstallParams:^(NSDictionary *params, NSURL *URL, NSError *error) {
              if (error) {
                  return;
              }
              if (URL.absoluteString.length > 0||params.count > 0) {
                   [MobClickLink handleLinkURL:URL delegate: (id<MobClickLinkDelegate>)self];
              }
              [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"key_Has_Get_InstallParams"];
          }];
      }else{
          //已经调用过getInstallParam方法,没必要在下次启动时再调用
          //但后续仍可在需要时调用,比如demo中的按钮点击
      }
  });
}
// 实现 MobClickLinkDelegate 的回调方法
- (void)getLinkPath:(NSString *)path params:(NSDictionary *)params{
  // 存储自符串tc
  NSString *tcValue = [params objectForKey:@"tc"];
  if(tcValue){
      // 重新加载一次webview 将tc传入 webview 内部的 h5
      [self loadWebView: tcValue];
  }
}
3、处理深度链接跳转过来的参数

在 SceneDelegate中处理深度链接唤起app的参数

// 处理深度链接 Universal link的回调
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
  // 处理链接
  [MobClickLink handleUniversalLink:userActivity delegate: (id<MobClickLinkDelegate>)self];
}


- (void)getLinkPath:(NSString *)path params:(NSDictionary *)params{
  NSString *tcValue = [params objectForKey:@"tc"];
  if(tcValue){
      // 重新加载一次webview
      [[[ViewController alloc] init] loadWebView: tcValue];
  }
}
3-4、android 接入友盟 sdk 代码
1、安装友盟库

手动下载最新的库,并存在app/libs下: image.png

AndroidManifest.xml 中增加权限

<!--  智能超链添加权限  -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--  用户复制粘贴权限 -->
<uses-permission android:name="android.permission.READ_CLIPBOARD" />

build.gradle.kts 文件中引入sdk

defaultConfig {
  //...
    ndk {
        abiFilters += listOf("armeabi-v7a","arm64-v8a","x86","x86_64")
    }
}
dependencies {
    implementation(fileTree("libs") { include("*.aar") })
}
2、初始化 SDK

在启动页的onCreate 初始化预启动配置

SplashActivity.kt

import com.umeng.commonsdk.UMConfigure
 override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    UMConfigure.preInit(this,"xxxxxxx","google play");
}

MainActivity.kt 中进行初始, google play 审核比较严格这块初始化不能简单通过延时调用,需要弹窗提示用户,用户同意授权后在调用

UMConfigure.init(
    this,
    "xxxxxx",
    "google play",
    UMConfigure.DEVICE_TYPE_PHONE,
    ""
)
3、配置schemeurl

AndroidManifext.xml 访问 kalodata://xxx 时可以唤起打开 app

<activity android:name=".MainActivity"
    android:exported="true"
    android:launchMode="standard">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="kalodata"/>
    </intent-filter>
</activity>
4、获取初始化参数
// 获取初始化参数
Handler(Looper.getMainLooper()).postDelayed({
    // 尝试访问剪贴板的代码
    val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    val clipData = clipboardManager.primaryClip
    if (clipData != null && clipData.itemCount > 0) {
        val item = clipData.getItemAt(0)
        val text = item.text?.toString() // 处理获取到的文本数据
        MobclickLink.getInstallParams(this@MainActivity,text, umlinkAdapter);
    }else{
        MobclickLink.getInstallParams(this@MainActivity,true, umlinkAdapter);
    }
}, 1000) // 延迟时间可以根据需要调整

MobclickLink.getInstallParams 是获取首次安装参数的方法当剪切板有文本还是没有文本分别处理

umlinkAdapter 是一个类含有不同的处理方法, 我抽离到 utils里 新建 MyUmengLinkAdapter.kt

package com.kalodata.kalodata_android.utils
import android.content.Intent
import android.net.Uri
import android.util.Log
import com.kalodata.kalodata_android.MainActivity
import com.umeng.umlink.UMLinkListener

class MyUmengLinkAdapter(private val mainActivityInstance: MainActivity) : UMLinkListener {
    override  fun onLink(path: String?, params: HashMap<String, String>?) {
        // 处理链接回调
        path?.let {
            // 处理path
            Log.d("onLink path", "" + path)
        }
        params?.let {
            // 处理参数
            Log.d("onLink params", "" + params)
        }
    }

    override fun onInstall(installParams: HashMap<String, String>?, uri: Uri?) {
        // 处理安装回调
        installParams?.let {
            // 处理安装参数
            Log.d("onLink installParams", "" + installParams)
        }
        uri?.let {
            // 创建一个Intent实例并设置其属性
            val intent = Intent(Intent.ACTION_VIEW).apply {
                data = uri // 设置URI作为Intent的数据
                addCategory(Intent.CATEGORY_BROWSABLE) // 添加BROWSABLE类别,通常用在VIEW Intent中
            }
            mainActivityInstance.handleIntent(intent)
        }
    }

    override fun onError(error: String?) {
        // 处理错误回调
        error?.let {
            // 处理错误
            Log.d("onLink error", "" + error)
        }
    }
}

使用

val umlinkAdapter = MyUmengLinkAdapter(this@MainActivity)
5、处理深度链接唤起app传参, 安卓里是通过 onNewIntent 方法实现的
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent) // 调用父类的方法
     handleIntent(intent)
}
6、处理Webview的参数传递,也就是在加载的地址后面拼接参数
// 处理新的intent
fun handleIntent(intent: Intent?) {
    val baseUrl = "https://www.baidu.com"
    // 检查 Intent 的 action
    val uri = intent?.data
    if (intent?.action == Intent.ACTION_VIEW) {
        // 获取 URI 数据
        uri?.let {
            val tc = it.getQueryParameter("tc")
            if (tc != null) {
                Log.d("onLink tc", tc)
            }
            Log.d("onLink tc", "tc")
            val finalUrl = if (tc != null && tc != "") {
                "$baseUrl?tc=$tc"
            } else {
                baseUrl // 如果不存在,使用基本 URL
            }
            Log.d("onLink finalUrl", finalUrl)
            loadWebView(finalUrl) // 调用加载 WebView 的方法
        }
    }else{
        loadWebView(baseUrl)
    }
}

private fun loadWebView(finalUrl: String){
    webView.loadUrl(finalUrl)
}
3-5、android 去掉友盟sdk 该用复制粘贴方案自己实现

在android上使用友盟超链在提交google play时,审核不通过, 当按照合规内容与隐私条款都严格按照官方提示更改后, 发现审核还是不给通过, 保险起见放弃了安卓上使用友盟sdk获取首次安装参数的方案, 选择通过本机的复制粘贴实现

image.png img_v3_02ed_c64387ca-bf18-4b84-bf5d-2aa08868d51g.jpg

正常使用友盟sdk也是会复制一段混淆参数的代码过来在经过内部方法解析,我这边直接改成h5那里修改支付复制参数,来这边粘贴替换友盟混淆的代码

修改 MainActivity.kt

Handler(Looper.getMainLooper()).postDelayed({
    // 尝试访问剪贴板的代码
    val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    val clipData = clipboardManager.primaryClip
    if (clipData != null && clipData.itemCount > 0) {
        val item = clipData.getItemAt(0)
        val text = item.text?.toString()  // 处理获取到的文本数据
        // 检查文本内容是否以 "tc=" 开头
            if (text != null && text.startsWith("tc=")) {
                handleIntent(intent, text);
            }else{
                handleIntent(intent,"");
            }
    }else{
        handleIntent(intent,"");
    }
}, 1000) // 延迟时间可以根据需要调整

修改h5复制代码 这里对安卓端特殊处理了,因为安卓端在发布google play审核无法通过,改成了复制参数,并在安卓端粘贴参数的方案实现

window.ULink({
  id: "用户渠道里生成的linkid",
  data: {
    tc,
  },
  selector: "#Go_TO_APP",
  timeout: 2000,
  useOpenInBrowerTips: "default",
  lazy: false,
  auto: false,
  useClipboard: false,
  proxyOpenDownload: myDownloadStyle,
  onready: function (ctx) {
    var u = navigator.userAgent;
    var isAndroid = u.indexOf("Android") > -1 || u.indexOf("Adr") > -1;
    // 这里对安卓端特殊处理了,因为安卓端在发布google play审核无法通过,改成了复制参数,并在安卓端粘贴参数的方案实现
    if (isAndroid) {
      if (storage.getItem(LOCALSTORAGE_KEY.TRACK_CODE)) {
        return setclipboardText("tc=" + storage.getItem(LOCALSTORAGE_KEY.TRACK_CODE));
      }
    }
    if (ctx.solution?.clipboardToken) {
      setclipboardText(ctx.solution.clipboardToken);
    }
  },
});

唤起时无需修改代码,仍然是通过intent的处理参数拼接到url上,同上面

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent) // 调用父类的方法
    handleIntent(intent,"")
}

// 处理新的intent
fun handleIntent(intent: Intent?, tc: String?) {
    var baseUrl = "https://www.baidu.com"
    // 检查 Intent 的 action
    val uri = intent?.data
    if (intent?.action == Intent.ACTION_VIEW) {
        // 获取 URI 数据
        uri?.let {
            val tc = it.getQueryParameter("tc")
            if (tc != null) {
                Log.d("onLink tc", tc)
            }
            Log.d("onLink tc", "tc")
            val finalUrl = if (tc != null && tc != "") {
                "$baseUrl?tc=$tc"
            } else {
                baseUrl // 如果不存在,使用基本 URL
            }
            Log.d("onLink finalUrl", finalUrl)
            loadWebView(finalUrl) // 调用加载 WebView 的方法
        }
    }else{
        if(tc !== ""){
            baseUrl = "$baseUrl?$tc"
        }
        loadWebView(baseUrl)
    }
}

四、总结

至此,用户来源相关、APP下载和唤起实现参数的传递就已经都解决了, 主要遇到的问题还是因为 ios 端的下载应用无法传参, 使用第三方sdk 也是主要解决这个问题, 原理就是ip或者利用剪切板模糊匹配,利用友盟超链可以方便、免费的实现该功能,并且在友盟后台还可以分析相关用户来源的信息,比如用户分布、设备情况等等, 功能还是很强大的, 但是需要注意的是,google play对合规问题要求比较高, 所以最好还是不要用国内的sdk