Android开发踩坑记录

Android开发踩坑记录

做Android开发多年,从权限管理到应用上架,踩了不少坑。记录一下实战经验和常见问题的解决方案。

危险权限管理

危险权限列表

Android 6.0+引入了运行时权限,危险权限涉及用户隐私,需要动态请求。

权限组 危险权限
CALENDAR READ_CALENDAR, WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE, CALL_PHONE, READ_CALL_LOG
SENSORS BODY_SENSORS
SMS SEND_SMS, RECEIVE_SMS, READ_SMS
STORAGE READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE

坑1:Android 10+存储权限变化

Android 10引入了Scoped Storage,存储权限行为发生变化:

1
2
3
4
5
6
7
8
9
10
11
// Android 10+ (API 29+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 使用MediaStore或SAF访问文件
} else {
// 传统方式
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 请求权限
}
}

运行时权限请求

AndroidManifest.xml声明

1
2
3
4
5
6
7
8
<manifest>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />

<!-- 如果应用不需要相机硬件,声明可选 -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>

Kotlin权限请求代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MainActivity : AppCompatActivity() {

companion object {
private const val PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS = mutableListOf(
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkAndRequestPermissions()
}

private fun checkAndRequestPermissions() {
val permissionsToRequest = REQUIRED_PERMISSIONS.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}

if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
PERMISSION_REQUEST_CODE
)
}
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
grantResults.forEachIndexed { index, result ->
if (result == PackageManager.PERMISSION_GRANTED) {
handlePermissionGranted(permissions[index])
} else {
handlePermissionDenied(permissions[index])
}
}
}
}
}

Activity Result API(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MainActivity : AppCompatActivity() {

private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// 权限已授予
} else {
// 权限被拒绝
}
}

private fun requestCameraPermission() {
when {
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED -> {
// 已有权限
}
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
// 显示权限说明对话框
showRationaleDialog()
}
else -> {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
}

权限申请最佳时机

场景 最佳时机
存储权限 用户点击”保存”按钮时
相机权限 打开相机前
位置权限 需要定位功能时
联系人权限 导入联系人时
短信权限 验证码自动填充时

应用自启动配置

坑2:国内系统自启动限制

国内Android系统(小米、华为、OPPO、vivo等)默认限制应用后台运行,用户需要手动开启”自启动”权限。

AutoStarter库使用

1
2
3
dependencies {
implementation 'com.github.judemanutd:autostarter:1.1.0'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkAutoStartPermission()
}

private fun checkAutoStartPermission() {
val helper = AutoStartPermissionHelper.getInstance()

if (helper.isAutoStartPermissionAvailable(this)) {
if (!helper.getAutoStartPermission(this)) {
showAutoStartDialog()
}
}
}

private fun showAutoStartDialog() {
AlertDialog.Builder(this)
.setTitle("需要自启动权限")
.setMessage("为保证消息推送及时送达,请在设置中开启自启动权限")
.setPositiveButton("去设置") { _, _ ->
AutoStartPermissionHelper.getInstance().getAutoStartPermission(this)
}
.setNegativeButton("取消", null)
.show()
}
}

各厂商自启动设置跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
object AutoStartUtils {

fun openAutoStartSettings(context: Context) {
when (getDeviceManufacturer()) {
"xiaomi" -> tryXiaomi(context)
"huawei" -> tryHuawei(context)
"oppo" -> tryOppo(context)
"vivo" -> tryVivo(context)
else -> tryGeneric(context)
}
}

private fun getDeviceManufacturer(): String {
return Build.MANUFACTURER.lowercase()
}

private fun tryXiaomi(context: Context) {
try {
val intent = Intent()
intent.component = ComponentName(
"com.miui.securitycenter",
"com.miui.permcenter.autostart.AutoStartManagementActivity"
)
context.startActivity(intent)
} catch (e: Exception) {
tryGeneric(context)
}
}

private fun tryHuawei(context: Context) {
try {
val intent = Intent()
intent.component = ComponentName(
"com.huawei.systemmanager",
"com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"
)
context.startActivity(intent)
} catch (e: Exception) {
tryGeneric(context)
}
}

private fun tryGeneric(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:${context.packageName}")
context.startActivity(intent)
}
}

邓白氏编码申请

坑3:邓白氏编码申请流程复杂

苹果开发者企业账号需要邓白氏编码,申请流程比较繁琐。

申请步骤

  1. 联系苹果支持

    • 访问 Apple Developer Contact
    • 选择”Membership and Account” → “D-U-N-S Number”
    • 填写表格提交申请
  2. 等待苹果邮件(约1个工作日):

    • 收到来自 chinadev@apple.com 的邮件
  3. 回复邮件提供企业信息

1
2
3
4
5
6
7
8
9
10
11
企业英文名称 [法人实体名称]:(如果是有限责任公司,英文名必须以 CO., LTD. 结尾)
企业中文名称:
税号或企业登记编号:
实际地址:
城市:
省/自治区/直辖市:
邮政编码:
电话号码:
国家或地区:
工作电话号码:
工作电子邮件:
  1. 邓白氏官网提交资料

  2. 等待审核通过(约1个工作日)

注意事项

  1. 公司英文名必须以”CO., LTD.”结尾(有限责任公司)
  2. 所有信息必须与营业执照完全一致
  3. 工作邮箱必须是公司分配的企业邮箱
  4. 整个流程通常需要5-10个工作日
  5. 一个公司只能申请一个邓白氏编码

开发者账号类型

类型 价格 适用对象 App Store上架
个人账号 $99/年 独立开发者 可以
公司账号 $99/年 企业/组织 可以
企业账号 $299/年 大型企业 不可以(内部使用)

Google Play上架

应用签名

1
2
# 生成签名密钥
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias

build.gradle配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
android {
signingConfigs {
release {
storeFile file("my-release-key.jks")
storePassword "password"
keyAlias "my-alias"
keyPassword "password"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

上架准备

  1. 注册Google Play开发者账号($25一次性费用)
  2. 准备应用签名密钥
  3. 生成应用包(AAB格式)
  4. 编写隐私政策文档
  5. 填写内容分级问卷
  6. 准备应用截图和描述

国内应用商店上架

商店 官网 审核周期
华为应用市场 appgallery.huawei.com 1-3天
小米应用商店 dev.mi.com 1-2天
OPPO应用商店 open.oppomobile.com 2-3天
vivo应用商店 dev.vivo.com.cn 2-3天
腾讯应用宝 open.qq.com 1-3天
百度手机助手 app.baidu.com 1-2天

通用上架材料

  • APK/AAB文件
  • 应用图标(多尺寸)
  • 应用截图(5-8张)
  • 应用描述
  • 隐私政策链接
  • ICP备案号(如涉及网络服务)
  • 软件著作权证书(推荐)

坑4:权限被拒绝处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun handlePermissionDenied(context: Activity, permission: String) {
when {
ActivityCompat.shouldShowRequestPermissionRationale(context, permission) -> {
AlertDialog.Builder(context)
.setTitle("需要权限")
.setMessage("该功能需要访问您的存储空间来保存文件")
.setPositiveButton("授权") { _, _ ->
ActivityCompat.requestPermissions(
context,
arrayOf(permission),
PERMISSION_REQUEST_CODE
)
}
.setNegativeButton("取消", null)
.show()
}
else -> {
AlertDialog.Builder(context)
.setTitle("权限已被拒绝")
.setMessage("请手动开启权限:设置 > 应用 > 权限")
.setPositiveButton("去设置") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:${context.packageName}")
context.startActivity(intent)
}
.show()
}
}
}

总结

Android开发常见坑点:

  1. Android 10+存储权限要用Scoped Storage
  2. 国内系统要处理自启动权限,各厂商跳转页面不同
  3. 邓白氏编码申请要准备完整企业信息,英文名格式要注意
  4. 权限申请时机要合理,不要启动就申请一堆权限
  5. Google Play上架要用AAB格式,配置好签名
  6. 国内商店上架需要ICP备案号和软著

Android权限管理和上架流程越来越规范,但国内系统适配还是要多测试。建议多准备几套UI文案,引导用户开启必要权限。