Tips:任何技术文章都有时效性,《从0到1手动打造移动广告归因系统》系列会尽力提供技术背景和环境,还请参考前务必看清文章发布时间,以免产生误导
量U的 Android SDK 在整个量U归因系统中处于前沿位置,扮演着数据采集的角色,负责为后端提供原始输入数据进行实时处理,为了精确测量App的安装量,SDK需要对设备标识符 IMEI 等数据进行客户端的持久化保存,而 Android SDK 的持久化逻辑如下
-
首先写SD卡的非沙盒路径
从Android Q(Android 10,对应 api 版本为29)开始,App被限制读写SD卡的公共空间(媒体文件除外),取而代之的是只能读写专属该App的沙盒空间,这个路径通常是 /sdcard/Android/data/包名/,你打开手机自带的文件管理器,一般都能看到这个路径,这一点与 /data/data/包名/ 这样的 internal 内部存储空间不同,/data/data/包名/ 通常需要 Root Explorer 这样的超级文件管理器才能访问到,同时与 internal 内部存储空间一样,SD卡的沙盒空间也会随App的卸载而删除,所以为了准确测量 App 的安装量,即便 App 被卸载重复安装多次,依然不会重复统计安装量——必须将设备信息写入SD卡的非沙盒路径 -
如果SD卡的非沙盒路径不能写,退而求其次写 internal 内部存储,然后依靠 AndroidID 、MAC地址等其他唯一设备标识来对设备进行安装排重
虽然名义上从 Android Q 开始就不建议访问SD卡的公共空间,但是为了给开发者预留适配 Android Q 的时间,谷歌保留了一条豁免规则,那就是在 AndroidManifest.xml 的 application 节点添加一个 requestLegacyExternalStorage 属性(不要依赖这条规则豁免,下个版本可能会失效)
android:requestLegacyExternalStorage="true"
添加这个属性后,可以开启旧的文件访问方式,所以 targetSdkVersion >=29 和 <29 是两种完全不同的代码书写方式
- targetSdkVersion < 29
app 的 build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.example.root.test"
minSdkVersion 26
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
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:26.+'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
implementation 'com.android.support:design:26.+'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.root.test">
<application
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"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>
Tips: Android Q 访问沙盒文件不再需要声明权限,但是访问非沙盒文件(包括媒体文件)都需要声明权限并动态申请权限
单独写一个申请权限的类,可以与 MainActivity.java 放在同一个目录下面
CheckPermission.java
package com.example.root.test;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.util.Log;
public class CheckPermission {
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static final String[] PERMISSIONS_STORAGE = {"android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE" };
private static final String TAG = CheckPermission.class.getName();
public static int verifyStoragePermissions(Activity activity) {
try {
//检测是否有写的权限
int permission = ActivityCompat.checkSelfPermission(activity,PERMISSIONS_STORAGE[1]);
if (permission != PackageManager.PERMISSION_GRANTED) {
//Log.e(TAG, String.valueOf(permission));//询问or 拒绝 -1 使用中允许 or 始终允许 0
// 没有写的权限,去申请写的权限,会弹出对话框
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE);
}
return permission;
} catch (Exception e) {
return 1;
}
}
}
MainActivity.java
package com.example.root.test;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
int checkPermissionResult = CheckPermission.verifyStoragePermissions(MainActivity.this);
if(checkPermissionResult == PackageManager.PERMISSION_GRANTED){
write2SDCard("a.txt","hello");
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* 授权回调函数
* @param requestCode
* @param permissions
* @param grantResults
*/
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
//权限已成功申请
//Toast.makeText(this,grantResults[0]+"",Toast.LENGTH_SHORT);
//Log.e(TAG, String.valueOf(grantResults[0]));
write2SDCard("a.txt","hello");
} else {
//用户拒绝授权
Toast.makeText(this, "无法获取SD卡读写权限", Toast.LENGTH_SHORT).show();
//finish();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
/**
* 写入SD卡私有方法
* @param fileName
* @param content
*/
private void write2SDCard(String fileName,String content){
//1、判断sd卡是否可用
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
//sd卡可用
//2、获取sd卡路径
try{
File sdFile = Environment.getExternalStorageDirectory();
//File sdFile = getExternalFilesDir(null);
File packageSDDir = new File(sdFile,"/com.example.root.test/");
if(!packageSDDir.exists() || !packageSDDir.isDirectory()){
boolean res = packageSDDir.mkdirs();//
//Log.e(TAG,String.valueOf(res));
}
File filePath = new File(packageSDDir,fileName);//sd卡下面的a.txt文件 参数 前面 是目录 后面是文件
if(!filePath.exists() || !filePath.isFile()){
filePath.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
fileOutputStream.write(content.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Tips: 使用新版的 Android Studio 4.2新建工程的时候,默认的 targetSdkVersion 就已经是30,如果强行手动调低,会使得编译无法通过,所以如果必要,可以使用低版本的 Android Studio,比如3.2版本,默认新建工程的targetSdkVersion是26
- targetSdkVersion >= 29
app.build
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "28"
defaultConfig {
applicationId "com.example.test_library"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.navigation:navigation-fragment:2.2.2'
implementation 'androidx.navigation:navigation-ui:2.2.2'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
AndroidManifest.xml
必须加上 android:requestLegacyExternalStorage="true"
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.test_library">
<application
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/Theme.Test_library"
android:requestLegacyExternalStorage="true"> <!-- 此行必加-->
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Test_library.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>
其他代码与 targetSdkVersion < 29 时一样
(完结)
网友评论