带你读《Android全埋点解决方案》之二:$AppViewScreen全埋点方案

简介: 本书系统讲解了Android全埋点的解决方案,特别是控件点击事件的全埋点采集,总结并归纳了如下8种解决方案,并且都提供了完整的项目源码,适合初级、中级、高级水平的Android开发工程师、技术经理、技术总监等阅读。

点击查看第一章
点击查看第三章

第2章

$AppViewScreen全埋点方案
$AppViewScreen事件,即页面浏览事件。在Android系统中,页面浏览其实就是指切换不同的Activity或Fragment(本书暂时只讨论切换Activity的情况)。对于一个 Activity,它的哪个生命周期执行了,代表该页面显示出来了呢?通过对 Activity生命周期的了解可知,其实就是onResume(Activity activity)的回调方法。所以,当一个Activity 执行到onResume(Activity activity)生命周期时,也就代表该页面已经显示出来了,即该页面被浏览了。我们只要自动地在onResume里触发$AppViewScreen事件,即可解决$AppViewScreen事件的全埋点。

2.1 关键技术Application.ActivityLifecycleCallbacks

ActivityLifecycleCallbacks是Application 的一个内部接口,是从 API 14(即Android 4.0)开始提供的。Application 类通过此接口提供了一系列的回调方法,用于让开发者可以对 Activity 的所有生命周期事件进行集中处理(或称监控)。我们可以通过Application类提供的registerActivityLifecycleCallback(ActivityLifecycleCallbacks callback)方法来注册 ActivityLifecycleCallbacks回调。
我们下面先看看Application.ActivityLifecycleCallbacks都提供了哪些回调方法。Application.ActivityLifecycleCallbacks接口定义如下:

public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity activity, Bundle savedInstanceState);
    void onActivityStarted(Activity activity);
    void onActivityResumed(Activity activity);
    void onActivityPaused(Activity activity);
    void onActivityStopped(Activity activity);
    void onActivitySaveInstanceState(Activity activity, Bundle outState);
    void onActivityDestroyed(Activity activity);

}
以 Activity的onResume(Activity activity)生命周期为例,如果我们注册了 Activity-LifecycleCallbacks回调,Android 系统会先回调 ActivityLifecycleCallbacks 的 onActivity-Resumed(Activity activity)方法,然后再执行Activity本身的onResume函数(请注意这个调用顺序,因为不同的生命周期的执行顺序略有差异)。通过registerActivityLifecycleCallback 方法名中的“register”字样可以知道,一个 Application 是可以注册多个 ActivityLifecycleCallbacks回调的,我们通过registerActivityLifecycleCallback方法的内部实现也可以证实这一点。
public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {

synchronized (mActivityLifecycleCallbacks) {
    mActivityLifecycleCallbacks.add(callback);
}

}
内部定义了一个list用来保存所有已注册的ActivityLifecycleCallbacks。

2.2原理概述

实现Activity的页面浏览事件,大家首先想到的是定义一个BaseActivity,然后让其他Activity继承这个 BaseActivity。这种方法理论上是可行的,但不是最优选择,有些特殊的场景是无法适应的。比如,你在应用程序里集成了一个第三方的库(比如 IM 相关的),而这个库里恰巧也包含 Activity,此时你是无法让这个第三方的库也去继承你的 BaseActivity(最起码驱使第三方服务商去做这件事的难度比较大)。所以,为了实现全埋点中的页面浏览事件,最优的方案还是基于我们上面讲的 Application.ActivityLifecycleCallbacks。
不过,使用Application.ActivityLifecycleCallbacks机制实现全埋点的页面浏览事件,也有一个明显的缺点,就是注册Application.ActivityLifecycleCallbacks 回调要求 API 14+。
在应用程序自定义的 Application类的 onCreate()方法中初始化埋点 SDK,并传入当前的Application 对象。埋点SDK 拿到 Application 对象之后,通过调用 Application的registerActivityLifecycleCallback(ActivityLifecycleCallbacks callback)方法注册Application.ActivityLifecycleCallbacks回调。这样埋点 SDK 就能对当前应用程序中所有的 Activity 的生命周期事件进行集中处理(监控)了。在注册的 Application.ActivityLifecycleCallbacks 的onActivityResumed(Activity activity)回调方法中,我们可以拿到当前正在显示的 Activity对象,然后调用 SDK 的相关接口触发页面浏览事件($AppViewScreen)即可。

2.3 案例

下面我们会详细介绍$AppViewScreen事件全埋点方案的实现步骤。
完整的项目源码可以参考以下网址:
https://github.com/wangzhzh/AutoTrackAppViewScreen
第1步:新建一个项目(Project)
在新建的项目中,会自动包含一个主 module,即:app。
第2步:创建 sdk module
新建一个 Android Library module,名称叫 sdk,这个模块就是我们的埋点 SDK模块。
第3步:添加依赖关系
app module需要依赖sdk module。可以通过修改app/build.gradle 文件,在其 dependencies节点中添加依赖关系:
apply plugin: 'com.android.application'

android {

compileSdkVersion 28
defaultConfig {
    applicationId "com.sensorsdata.analytics.android.app.appviewscreen"
    minSdkVersion 15
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
}
buildTypes {
    release {
        minifyEnabled false
         proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

}

dependencies {

implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'

implementation project(':sdk')
}

也可以通过 Project Structure 给模块添加依赖关系,在此不再详细描述。
第4步:编写埋点 SDK
在sdk module 中我们新建一个埋点 SDK 的主类,即SensorsDataAPI.java,完整的源码参考如下:
package com.sensorsdata.analytics.android.sdk;

import android.app.Application;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import org.json.JSONObject;

import java.util.Map;

image.png


@Keep
public class SensorsDataAPI {
private final String TAG = this.getClass().getSimpleName();
public static final String SDK_VERSION = "1.0.0";
private static SensorsDataAPI INSTANCE;
private static final Object mLock = new Object();
private static Map<String, Object> mDeviceInfo;
private String mDeviceId;

@Keep
@SuppressWarnings("UnusedReturnValue")
public static SensorsDataAPI init(Application application) {
    synchronized (mLock) {
        if (null == INSTANCE) {
            INSTANCE = new SensorsDataAPI(application);
        }
        return INSTANCE;
    }
}

@Keep
public static SensorsDataAPI getInstance() {
    return INSTANCE;
}

private SensorsDataAPI(Application application) {
    mDeviceId = SensorsDataPrivate.getAndroidID(application.getApplicationContext());
    mDeviceInfo = SensorsDataPrivate.getDeviceInfo(application.getApplicationContext());
    SensorsDataPrivate.registerActivityLifecycleCallbacks(application);
}

![image.png](https://ucc.alicdn.com/pic/developer-ecology/656f122bf70b4d17bda922da776375a2.png)

image.png

public void track(@NonNull String eventName, @Nullable JSONObject properties) {
    try {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("event", eventName);
        jsonObject.put("device_id", mDeviceId);

        JSONObject sendProperties = new JSONObject(mDeviceInfo);

        if (properties != null) {
            SensorsDataPrivate.mergeJSONObject(properties, sendProperties);
        }

        jsonObject.put("properties", sendProperties);
        jsonObject.put("time", System.currentTimeMillis());

        Log.i(TAG, SensorsDataPrivate.formatJson(jsonObject.toString()));
        } catch (Exception e) {
            e.printStackTrace();
        }
}

}
目前这个主类比较简单,主要包含如下几个方法。
□init(Application application)
这是一个静态方法,是埋点SDK的初始化函数,有一个Application类型的参数。内部实现使用到了单例设计模式,然后调用私有构造函数初始化埋点 SDK。app module 就是调用这个方法来初始化我们的埋点SDK。
□getInstance()
它也是一个静态方法,app 通过该方法可以获取埋点 SDK 的实例对象。
□SensorsDataAPI(Application application)
私有的构造函数,也是埋点 SDK 真正的初始化逻辑。在其方法内部通过调用 SDK 的内部私有类SensorsDataPrivate中的方法来注册ActivityLifecycleCallbacks。
□track(@NonNull final String eventName, @Nullable JSONObject properties)
对外公开的 track 事件接口。通过调用该方法可以触发事件,第一个参数 eventName 代表事件名称,第二个参数properties代表事件属性。本书为了简化,触发事件仅仅通过Log.i打印了事件的JSON信息。
关于SensorsDataPrivate类中的getAndroidID(Context context)、getDeviceInfo(Context context)、mergeJSONObject(final JSONObject source, JSONObject dest)、formatJson(String jsonStr)方法实现可以参考如下源码:
package com.sensorsdata.analytics.android.sdk;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActionBar;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.Keep;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.DisplayMetrics;

import org.json.JSONException;
import org.json.JSONObject;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
image.png
image.png
image.png
image.png
image.png
image.png
image.png

第5步:注册 ActivityLifecycleCallbacks回调
我们是通过调用 SDK 的内部私有类SensorsDataPrivate的registerActivityLifecycleCallbacks(Application application)方法来注册ActivityLifecycleCallbacks的。
image.png

@TargetApi(14)
public static void registerActivityLifecycleCallbacks(Application application) {

application.registerActivityLifecycleCallbacks(new Application.Activity-LifecycleCallbacks() {
    @Override
    public void onActivityCreated(final Activity activity, Bundle bundle) {
    }
    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(final Activity activity) {
        trackAppViewScreen(activity);
     }

    @Override
    public void onActivityPaused(Activity activity) {
    }

    @Override
    public void onActivityStopped(Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    }
});

}
需要我们注意的是,只有 API 14+ 才能注册ActivityLifecycleCallbacks回调。
在ActivityLifecycleCallbacks的onActivityResumed(final Activity activity)回调方法中,我们通过调用SensorsDataPrivate的trackAppViewScreen(Activity activity)方法来触发页面浏览事件($AppViewScreen)。
trackAppViewScreen(Activity activity)方法的内部实现逻辑比较简单,可以参考如下:
image.png

@Keep
private static void trackAppViewScreen(Activity activity) {

try {
    JSONObject properties = new JSONObject();
    properties.put("$activity", activity.getClass().getCanonicalName());
    SensorsDataAPI.getInstance().track("$AppViewScreen", properties);
} catch (Exception e) {
    e.printStackTrace();
}

}
在此示例中,我们添加了一个$activity 属性,代表当前 Activity 的名称,我们使用包名+类名的形式表示。然后又定义了事件名称为“$AppViewScreen”,最后调用Sensors-DataAPI的 track 方法来触发页面浏览事件。
第6步:初始化埋点 SDK
需要在应用程序自定义的 Application类中初始化埋点 SDK,一般是建议在 onCreate()方法中初始化。
package com.sensorsdata.analytics.android.app;

import android.app.Application;

import com.sensorsdata.analytics.android.sdk.SensorsDataAPI;

public class MyApplication extends Application {

@Override
public void onCreate() {
    super.onCreate();
    initSensorsDataAPI(this);
}

image.png

private void initSensorsDataAPI(Application application) {
    SensorsDataAPI.init(application);
}

}
第7步:声明自定义的 Application
以上面定义的MyApplication为例,需要在AndroidManifest.xml文件的application节点中声明MyApplication。
<?xml version="1.0" encoding="utf-8"?>
image.png
image.png

运行 demo并启动一个 Activity,可以看到如下打印的事件信息,参考图2-1。

image.png

上面的事件名称叫“$AppViewScreen”,代表的是页面浏览事件,它有一个自定义属性,叫“$activity”,代表当前正在显示的 Activity 名称(包名+类名)。
至此,页面浏览事件($AppViewScreen)的全埋点方案就算完成了。

2.4 完善方案

在Android 6.0(API 23)发布的同时又引入了一种新的权限机制,即Runtime Permissions,又称运行时权限。
在一般情况下,我们如果要使用 Runtime Permissions主要分为四个步骤,下面我们以使用(申请)“android.permission.READ_CONTACTS”权限为例来介绍。
第1步:声明权限
需要在AndroidManifest.xml文件中使用uses-permission声明应用程序要使用的权限列表。
<?xml version="1.0" encoding="utf-8"?>

package="com.sensorsdata.analytics.android.app">

<uses-permission android:name="android.permission.READ_CONTACTS" />

<application
    android:name=".MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>


第2步:检查权限
如果应用程序需要使用 READ_CONTACTS 权限,则要在每次真正使用 READ_CONTACTS 权限之前,检测当前应用程序是否已经拥有该权限,这是因为用户可能随时会在Android 系统的设置中关掉授予当前应用程序的任何权限。检测权限可以使用ContextCompat的checkSelfPermission方法,简单示例如下:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) ==
    PackageManager.PERMISSION_GRANTED) {
//拥有权限

} else {

//没有权限,需要申请权限

}
其中,PackageManager.PERMISSION_GRANTED代表当前应用程序已经拥有了该权限;反之,PackageManager.PERMISSION_DENIED 代表当前应用程序没有获得该权限,需要再次申请。
第3步:申请权限
可以通过调用ActivityCompat的requestPermissions方法来申请一个或者一组权限,简单示例如下:
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS},

                   PERMISSIONS_REQUEST_READ_CONTACTS);

调用ActivityCompat.requestPermissions方法之后,系统会弹出如图2-2的请求权限对话框(该对话框可能会随着 ROM的不同而略有差异):

image.png

第4步:处理权限请求结果
用户选择之后的结果会回调当前 Activity的onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)方法,我们可以根据 requestCode和grantResults参数来判断用户选择了“允许”还是“禁止”按钮。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

switch (requestCode) {
    case PERMISSIONS_REQUEST_READ_CONTACTS:
        if (grantResults.length > 0 &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            //用户点击允许
        } else {
            //用户点击禁止
        }
        break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

}
讲到这里,你肯定开始疑惑了,这跟采集页面浏览事件有什么关系呢?
其实是有关系的!我们继续往下看。
通过测试可以发现,我们调用ActivityCompat.requestPermissions方法申请权限之后,不管用户选择了“允许”还是“禁止”按钮,系统都会先调用onRequestPermissionsResult回调方法,然后再调用当前 Activity 的 onResume 生命周期函数。而我们上面介绍的,就是通过 onResume生命周期函数来采集页面浏览事件的,这个现象会直接导致我们的埋点 SDK 再一次触发页面浏览事件。
对于这个问题,我们该如何解决呢?事实上,虽然目前也没有非常完美的解决方案,但是我们还是可以借助其他方法来尝试解决。毕竟,在一个完整的应用程序中,真正需要申请权限的页面并不是很多。所以,我们可以在这些申请权限的页面里进行一些特殊的“操作”来规避上面的问题。
我们可以考虑给埋点 SDK 新增一个功能,即用户可以设置想要过滤哪些 Activity 的页面浏览事件(即指定不采集哪些 Activity 的页面浏览事件),然后通过灵活使用这个接口,解决上面的问题。
下面我们详细地介绍一下具体的实现步骤。
第1步:在SensorsDataAPI中新增两个接口
image.png

□ignoreAutoTrackActivity(Class<?> activity)
指定忽略采集哪个 Activity 的页面浏览事件。
□removeIgnoredActivity(Class<?> activity)
指定恢复采集哪个 Activity 的页面浏览事件。
以上两个接口,都是调用私有类SensorsDataPrivate中相对应的方法。
package com.sensorsdata.analytics.android.sdk;

......

image.png

static {
    mIgnoredActivities = new ArrayList<>();
}
public static void ignoreAutoTrackActivity(Class<?> activity) {
    if (activity == null) {
        return;
    }

    mIgnoredActivities.add(activity.getClass().getCanonicalName());
}

public static void removeIgnoredActivity(Class<?> activity) {
    if (activity == null) {
        return;
    }

    if (mIgnoredActivities.contains(activity.getClass().getCanonicalName())) {
        mIgnoredActivities.remove(activity.getClass().getCanonicalName());
    }
}

......
}
内部实现机制比较简单,仅仅通过定义一个List来保存忽略采集页面浏览事件的 Activity 的名称(包名+类名)。
第2步:修改trackAppViewScreen(Activity activity)方法添加相应的判断逻辑
image.png

@Keep
private static void trackAppViewScreen(Activity activity) {

try {
    if (activity == null) {
        return;
    }
    if (mIgnoredActivities.contains(activity.getClass().getCanonicalName())) {
        return;
    }
    JSONObject properties = new JSONObject();
    properties.put("$activity", activity.getClass().getCanonicalName());
    SensorsDataAPI.getInstance().track("$AppViewScreen", properties);
} catch (Exception e) {
    e.printStackTrace();
}

}
首先判断当前Activity是否已经被忽略,如果被忽略,则不触发页面浏览事件,否则将触发页面浏览事件。
第3步:修改申请权限的 Activity
在申请权限的 Activity中,在它的onRequestPermissionsResult回调中首先调用ignoreAutoTrackActivity方法来忽略当前 Activity 的页面浏览事件,然后在 onStop 生命周期函数中恢复采集当前 Activity 的页面浏览事件。
package com.sensorsdata.analytics.android.app;

import android.Manifest;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.sensorsdata.analytics.android.sdk.SensorsDataAPI;

public class MainActivity extends AppCompatActivity {

private final static int PERMISSIONS_REQUEST_READ_CONTACTS = 100;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    setTitle("Home");

    if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) ==
            PackageManager.PERMISSION_GRANTED) {
        //拥有权限
    } else {
        //没有权限,需要申请全新啊
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission. READ_CONTACTS},
                PERMISSIONS_REQUEST_READ_CONTACTS);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    SensorsDataAPI.getInstance().ignoreAutoTrackActivity(MainActivity.class);
    switch (requestCode) {
        case PERMISSIONS_REQUEST_READ_CONTACTS:
            if (grantResults.length > 0 &&

grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // 用户点击允许
            } else {
                // 用户点击禁止
            }
            break;
    }

    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

@Override
protected void onStop() {
    super.onStop();
    SensorsDataAPI.getInstance().removeIgnoredActivity(MainActivity.class);
}

}
这样处理之后,就可以解决申请权限再次触发页面浏览事件的问题了。

2.5 扩展采集能力

对于Activity的页面浏览事件,仅仅采集当前 Activity 的名称(包名 + 类名)是远远不够的,还需要采集当前 Activity 的 title(标题)才能满足实际的分析需求。
但是一个 Activity 的 title 的来源是非常复杂的,因为可以通过不同的方式来设置一个 Activity 的 title,甚至可以使用自定义的 View 来设置 title。比如说,可以在Android-Manifest.xml文件中声明 activity 时通过 android:label属性来设置,还可以通过 activity.setTitle()来设置,也可以通过 ActionBar、ToolBar 来设置。所以,在获取Activity 的 title 时,需要兼容不同的设置title的方式,同时更需要考虑其优先级顺序。
我们目前写了一个比较简单的方法来获取一个 Activity 的 title,内容参考如下:
image.png

public static String getActivityTitle(Activity activity) {

String activityTitle = null;

if (activity == null) {
    return null;
}

try {
    activityTitle = activity.getTitle().toString();

    if (Build.VERSION.SDK_INT >= 11) {
        String toolbarTitle = getToolbarTitle(activity);
        if (!TextUtils.isEmpty(toolbarTitle)) {
            activityTitle = toolbarTitle;
        }
    }

    if (TextUtils.isEmpty(activityTitle)) {
        PackageManager packageManager = activity.getPackageManager();
        if (packageManager != null) {
            ActivityInfo activityInfo = packageManager.getActivityInfo(activity.getComponentName(), 0);
            if (activityInfo != null) {
                activityTitle = activityInfo.loadLabel(packageManager).toString();
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}
return activityTitle;

}
我们首先通过activity.getTitle() 获取当前 Activity 的 title,因为用户有可能会使用 ActionBar 或 ToolBar,所以我们还需要获取 ActionBar 或 ToolBar 设置的 title,如果能获取到,就以这个为准(即覆盖通过activity.getTitle()获取的 title)。如果以上两个步骤都没有获取到 title,那我们就要尝试获取 android:label 属性的值。
获取ActionBar或ToolBar的title逻辑如下:
@TargetApi(11)
private static String getToolbarTitle(Activity activity) {

try {
    ActionBar actionBar = activity.getActionBar();
    if (actionBar != null) {
        if (!TextUtils.isEmpty(actionBar.getTitle())) {
            return actionBar.getTitle().toString();
        }
    } else {
        if (activity instanceof AppCompatActivity) {
            AppCompatActivity appCompatActivity = (AppCompatActivity) activity;
            android.support.v7.app.ActionBar supportActionBar = appCompat-Activity.getSupportActionBar();
            if (supportActionBar != null) {
                if (!TextUtils.isEmpty(supportActionBar.getTitle())) {
                    return supportActionBar.getTitle().toString();
                }
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}
return null;

}
修改trackAppViewScreen(Activity activity)方法,添加设置$title 属性的逻辑:
image.png

@Keep
private static void trackAppViewScreen(Activity activity) {

try {
    if (activity == null) {
        return;
    }
    if (mIgnoredActivities.contains(activity.getClass().hashCode())) {
        return;
    }
    JSONObject properties = new JSONObject();
    properties.put("$activity", activity.getClass().getCanonicalName());
    properties.put("$title", getActivityTitle(activity));
    SensorsDataAPI.getInstance().track("$AppViewScreen", properties);
} catch (Exception e) {
    e.printStackTrace();
}

}
运行 demo,可以看到打印的如下事件信息,参考图2-3。

image.png

至此,一个相对完善的用来采集页面浏览事件的全埋点方案就算完成了。

相关文章
|
3天前
|
安全 编译器 API
Android HAL深入探索(5): 调试HAL报错与解决方案
Android HAL深入探索(5): 调试HAL报错与解决方案
6 1
|
1月前
|
JSON Android开发 数据格式
android 使用GSON 序列化对象出现字段被优化问题解决方案
android 使用GSON 序列化对象出现字段被优化问题解决方案
|
8月前
|
IDE Java 开发工具
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8的解决方案
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8的解决方案
|
7月前
|
Android开发
Android Studio 控制台中文乱码,解决方案都在这里了,完美解决
Android Studio 控制台中文乱码,解决方案都在这里了,完美解决
|
7月前
|
存储 缓存 前端开发
Android Github 上面优秀的两种阴影方案,完美兼容高低版本问题
Android Github 上面优秀的两种阴影方案,完美兼容高低版本问题
|
8月前
|
Android开发
Android 中ViewPager嵌套RecyclerView出现滑动冲突的解决方案
Android 中ViewPager嵌套RecyclerView出现滑动冲突的解决方案
705 0
|
8月前
|
Android开发
Android > Project with path ‘:audiovisualize‘ could not be found in project ‘:app‘. 异常解决方案
Android > Project with path ‘:audiovisualize‘ could not be found in project ‘:app‘. 异常解决方案
42 0
|
8月前
|
Web App开发 编解码 网络协议
Android平台一对一音视频通话方案对比:WebRTC VS RTMP VS RTSP
Android平台一对一音视频通话方案对比:WebRTC VS RTMP VS RTSP
283 0
|
9月前
|
网络协议 Linux Android开发
Android部分手机4G网第一次请求很慢(wifi正常)解决方案
Android部分手机4G网第一次请求很慢(wifi正常)解决方案
255 0