Android游戏开发踩坑记录

Android游戏开发踩坑记录

记录实际游戏开发中遇到的Android相关问题,包括签名、线程、审核、防沉迷等。

Android开发常见问题

1.1 签名与打包问题

问题:INSTALL_FAILED_INVALID_APK 签名不一致

1
2
Failed to finalize session : INSTALL_FAILED_INVALID_APK:
/data/app/dependencies signatures are inconsistent

原因分析:

  • 使用了自定义签名文件,与系统默认签名不一致
  • 多次安装使用了不同的签名文件
  • 缓存数据未清理导致签名冲突

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
# 步骤1:清理项目
# 在Android Studio中点击 Build -> Clean Project

# 步骤2:重新构建
Build -> Rebuild Project

# 步骤3:如果仍然失败,尝试完全卸载
# 在设备上卸载应用,或运行:
adb uninstall com.your.package.name

# 步骤4:重新安装
adb install app-release.apk

预防措施:

  1. 统一使用release签名进行测试
  2. 配置签名文件版本控制
  3. 建立签名文件管理制度

1.2 UI线程操作规范

问题:Looper.prepare() 线程错误

1
2
java.lang.RuntimeException: Can't create handler inside thread
that has not called Looper.prepare()

原因分析:
Android中UI操作必须在主线程(UI线程)执行,从后台线程直接操作UI组件会抛出异常。

解决方案:

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
// 错误做法 - 从后台线程直接操作UI
new Thread() {
public void run() {
// 这会抛出异常!
textView.setText("更新文本");
}
}.start();

// 正确做法1 - 使用runOnUiThread
new Thread() {
public void run() {
Activity.runOnUiThread(new Runnable() {
public void run() {
textView.setText("更新文本");
}
});
}
}.start();

// 正确做法2 - 使用Handler
Handler handler = new Handler(Looper.getMainLooper());
new Thread() {
public void run() {
handler.post(() -> {
textView.setText("更新文本");
});
}
}.start();

// 正确做法3 - 使用AsyncTask(已废弃,建议使用ExecutorService)
// 正确做法4 - 使用Kotlin Coroutines(推荐)
lifecycleScope.launch(Dispatchers.IO) {
// 后台操作
val result = fetchData()
withContext(Dispatchers.Main) {
// UI操作
textView.text = result
}
}

1.3 屏幕常亮设置

在游戏开发中,经常需要保持屏幕常亮,防止游戏过程中自动熄屏。

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GameActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);

// 保持屏幕常亮
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}

@Override
protected void onDestroy() {
super.onDestroy();
// 移除标志(可选)
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}

替代方案 - 使用WakeLock(不推荐):

1
2
3
4
5
6
7
// 需要权限:<uses-permission android:name="android.permission.WAKE_LOCK" />
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "Game:WakeLock");
wakeLock.acquire(10*60*1000L); // 10分钟

// 记得释放
wakeLock.release();

推荐使用FLAG_KEEP_SCREEN_ON原因:

  • 系统自动管理,应用不在前台时自动释放
  • 不需要额外权限
  • 符合Android最佳实践

1.4 版本兼容性处理

Android版本与SDK版本对照表:

平台版本 SDK版本 版本名称 发布时间
Android 8.0 26 Oreo 2017
Android 7.1 25 Nougat 2016
Android 6.0 23 Marshmallow 2015
Android 5.1 22 Lollipop 2015
Android 4.4 19 KITKAT 2013

build.gradle配置:

1
2
3
4
5
6
7
8
9
10
11
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"

defaultConfig {
minSdkVersion 19 // 支持Android 4.4及以上
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
}

1.5 资源访问权限

Windows路径过长导致编译失败:

1
/jswrapper/v8/debugger/inspector_socket.o] Error 3

解决方案:

1
2
3
4
5
6
# 将项目移动到根目录下的短路径
# 从:C:/Users/Administrator/Desktop/cocos/hello_world/build/
# 改为:C:/build/

# Windows最大路径限制:260字符
# 使用短路径可以避免编译失败

应用商店审核技巧

苹果App Store审核要点

隐私政策配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<title>隐私政策</title>
</head>
<body>
<h1>隐私政策</h1>
<p>本应用尊重并保护所有用户的个人隐私权...</p>

<h2>信息收集</h2>
<p>我们收集的信息包括:</p>
<ul>
<li>设备标识符</li>
<li>游戏进度数据</li>
<li>匿名统计信息</li>
</ul>

<h2>信息使用</h2>
<p>收集的信息仅用于改善用户体验...</p>
</body>
</html>

宣传素材要求:

素材类型 要求 注意事项
截图 3-5张 无争议性描述
应用图标 1024x1024 无”提现”等敏感词
预览视频 15-30秒 展示核心玩法

关键词设置建议:

1
2
推荐关键词:阳光、开心、欢乐、快乐、萌、可爱、合成、养成
避免关键词:提现、赚钱、红包、现金、赚钱

审核代码混淆

需要屏蔽的关键字:

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
-proguard-rules.pro:

# 屏蔽支付相关
-pay
-cash
-wallet

# 屏蔽Web相关
-h5
-webview
-web
-http

# 屏蔽操作相关
-close
-open
-show
-hide
-switch
-change

# 屏蔽订单相关
-order
-page
-url
-type

ProGuard配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 基本混淆
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontskipnonpubliclibraryclassmembers
-dontpreverify
-verbose
-printmapping proguardMapping.txt
-optimizations !code/simplification/arithmetic,!field/*,!class/merger/*
-keepattributes *Annotation*,InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable

# 屏蔽敏感字段
-renamesourcefileattribute SourceFile

审核包策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ReviewConfig {
// 审核开关
private static final boolean IS_REVIEW_VERSION = true;

// 审核时间窗口
public static boolean isReviewTime() {
if (IS_REVIEW_VERSION) {
// 审核期间返回固定时间
return System.currentTimeMillis() <
parseTime("2099-12-31 23:59:59");
}
// 正常版本读服务器配置
return ConfigManager.getInstance().isReviewMode();
}

// 功能开关
public static boolean isAdEnabled() {
return !isReviewTime() && ConfigManager.getInstance().isAdEnabled();
}

public static boolean isPayEnabled() {
return !isReviewTime() && ConfigManager.getInstance().isPayEnabled();
}
}

防沉迷系统实现

法规要求

根据国家新闻出版署《关于防止未成年人沉迷网络游戏的通知》,游戏必须实现:

实名注册制度:

  • 所有用户必须使用有效身份信息注册
  • 建立并实施用户实名注册系统
  • 2个月内完成存量用户实名认证

游戏时长限制:

时段 未成年人可用
每日22:00-次日8:00 禁止
法定节假日 累计不超过3小时
其他时间 累计不超过1.5小时

付费限制:

年龄 单次限额 月累计限额
未满8周岁 禁止付费 禁止付费
8-16周岁 50元 200元
16-18周岁 100元 400元

实名认证系统架构

1
2
3
4
游戏客户端 -> 实名认证服务 -> 公安接口
|
v
数据库

实名认证实现

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
public class RealNameAuthManager {

private static final String AUTH_URL = "https://api.realname.example.com/verify";

public AuthResult authenticate(String name, String idCard) {
// 1. 验证身份证格式
if (!IDCardValidator.isValid(idCard)) {
return AuthResult.fail("身份证号码格式错误");
}

// 2. 调用实名认证接口
AuthResponse response = callAuthAPI(name, idCard);

// 3. 解析年龄
int age = IDCardParser.getAge(idCard);

// 4. 保存用户信息
saveUserAuthInfo(name, idCard, age, response.isSuccess());

return response.isSuccess() ? AuthResult.success(age) : AuthResult.fail("认证失败");
}

public boolean isMinor(String userId) {
UserAuthInfo info = getUserAuthInfo(userId);
if (info == null || !info.isAuthenticated()) {
// 未实名用户视为未成年人
return true;
}
return info.getAge() < 18;
}
}

游戏时长控制

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
public class PlayTimeController {

private static final int MINOR_DAILY_LIMIT = 90; // 1.5小时 = 90分钟
private static final int MINOR_HOLIDAY_LIMIT = 180; // 3小时 = 180分钟
private static final int NIGHT_START = 22; // 22:00
private static final int NIGHT_END = 8; // 次日8:00

public GameCheckResult checkPlayPermission(String userId) {
// 1. 检查是否是未成年人
if (!RealNameAuthManager.getInstance().isMinor(userId)) {
return GameCheckResult.allow();
}

// 2. 检查宵禁时间
if (isCurfewTime()) {
return GameCheckResult.deny("22:00-次日8:00期间未成年人禁止游戏");
}

// 3. 检查当日游戏时长
int todayPlayTime = getTodayPlayTime(userId);
int limit = isHoliday() ? MINOR_HOLIDAY_LIMIT : MINOR_DAILY_LIMIT;

if (todayPlayTime >= limit) {
return GameCheckResult.deny("今日游戏时间已满");
}

// 4. 设置剩余时间提醒
int remaining = limit - todayPlayTime;
if (remaining <= 15) {
return GameCheckResult.allowWithWarning("剩余游戏时间:" + remaining + "分钟");
}

return GameCheckResult.allow();
}

private boolean isCurfewTime() {
Calendar now = Calendar.getInstance();
int hour = now.get(Calendar.HOUR_OF_DAY);
return hour >= NIGHT_START || hour < NIGHT_END;
}

public void recordPlayTime(String userId, int minutes) {
String today = getTodayKey();
String key = "play_time:" + userId + ":" + today;
RedisClient.incrBy(key, minutes);
RedisClient.expire(key, 86400); // 24小时后过期
}
}

付费限制

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
public class PaymentLimitController {

public PaymentResult checkPayment(String userId, int amount) {
UserAuthInfo authInfo = RealNameAuthManager.getInstance().getUserAuthInfo(userId);

// 未实名用户禁止充值
if (authInfo == null || !authInfo.isAuthenticated()) {
return PaymentResult.fail("请先完成实名认证");
}

int age = authInfo.getAge();

// 根据年龄检查限制
if (age < 8) {
return PaymentResult.fail("未满8周岁用户禁止充值");
}

// 检查单次限额
int singleLimit = getSingleLimit(age);
if (amount > singleLimit) {
return PaymentResult.fail("单次充值超过" + singleLimit + "元限制");
}

// 检查月累计限额
int monthlySpent = getMonthlySpent(userId);
int monthlyLimit = getMonthlyLimit(age);
if (monthlySpent + amount > monthlyLimit) {
int remaining = monthlyLimit - monthlySpent;
return PaymentResult.fail("本月充值额度仅剩" + remaining + "元");
}

return PaymentResult.success();
}

private int getSingleLimit(int age) {
if (age < 16) return 50;
if (age < 18) return 100;
return Integer.MAX_VALUE;
}

private int getMonthlyLimit(int age) {
if (age < 16) return 200;
if (age < 18) return 400;
return Integer.MAX_VALUE;
}
}

游客模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GuestModeManager {

private static final int GUEST_LIMIT_MINUTES = 60; // 1小时
private static final int GUEST_COOLDOWN_DAYS = 15; // 15天内不能重复体验

public boolean allowGuestExperience(String deviceId) {
// 1. 检查是否超过15天
long lastExperience = getLastGuestExperience(deviceId);
if (lastExperience > 0) {
long daysPassed = (System.currentTimeMillis() - lastExperience) / (1000 * 60 * 60 * 24);
if (daysPassed < GUEST_COOLDOWN_DAYS) {
return false;
}
}

// 2. 检查当日体验时长
int todayExperience = getTodayGuestExperienceTime(deviceId);
return todayExperience < GUEST_LIMIT_MINUTES;
}

public void endGuestExperience(String deviceId) {
setLastGuestExperience(deviceId, System.currentTimeMillis());
}
}

防外挂方案

外挂类型

外挂类型 实现方式 危害程度
内存修改器 修改游戏内存数值
加速器 修改系统时间流速
自动脚本 模拟用户操作
脱机外挂 绕过客户端验证 极高

防护策略

降低重复性任务收益

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AntiCheatRewardManager {

private static final int MAX_DAILY_REWARD = 1000;
private static final float DECAY_RATE = 0.9f;

public int calculateReward(String userId, int baseReward, int times) {
// 同类型任务次数越多,收益递减
float multiplier = (float) Math.pow(DECAY_RATE, times);
int actualReward = (int) (baseReward * multiplier);

// 检查当日上限
int todayReward = getTodayReward(userId);
if (todayReward + actualReward > MAX_DAILY_REWARD) {
actualReward = Math.max(0, MAX_DAILY_REWARD - todayReward);
}

return actualReward;
}
}

协议层加密

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
public class SecureProtocol {

private static final String SECRET_KEY = "your-secret-key";

public static String encrypt(String data) {
try {
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.encodeToString(encrypted, Base64.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static String decrypt(String encryptedData) {
try {
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.decode(encryptedData, Base64.DEFAULT);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

服务器端数据校验

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
public class DataValidator {

public boolean validateBattleResult(BattleRequest request, BattleResult result) {
// 1. 验证战斗时间
int expectedDuration = calculateExpectedDuration(request);
int actualDuration = result.getDuration();
if (Math.abs(actualDuration - expectedDuration) > 10) {
return false;
}

// 2. 验证伤害数值
int maxPossibleDamage = calculateMaxDamage(request.getPlayerPower());
if (result.getTotalDamage() > maxPossibleDamage * 1.5) {
return false;
}

// 3. 验证资源获得
int expectedReward = calculateExpectedReward(request, result);
if (result.getReward() > expectedReward * 2) {
return false;
}

return true;
}
}

行为检测系统

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
public class BehaviorAnalyzer {

public RiskLevel analyzeBehavior(String userId, List<Action> actions) {
int riskScore = 0;

// 操作频率检测
double avgInterval = calculateAvgInterval(actions);
if (avgInterval < 50) { // 操作间隔过短
riskScore += 30;
}

// 操作规律性检测
double regularity = calculateRegularity(actions);
if (regularity > 0.9) { // 过于规律
riskScore += 40;
}

// 异常时间检测
if (hasAbnormalTiming(actions)) {
riskScore += 30;
}

if (riskScore >= 80) return RiskLevel.HIGH;
if (riskScore >= 50) return RiskLevel.MEDIUM;
return RiskLevel.LOW;
}
}

第三方防护SDK

腾讯手游安全方案:

功能 说明 价格
内存修改器防护 检测并阻止内存修改 5000元/月
加速器防护 检测并阻止变速作弊 5000元/月
设备指纹 识别异常设备 包含在基础服务
数据监控 实时外挂数据监控 包含在基础服务

接入代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SecuritySDK {

static {
TssSdk.init(context, "your-app-id", new TssCallback() {
@Override
public void onInitSuccess() {
Log.i("Security", "安全SDK初始化成功");
}

@Override
public void onInitFailure(int code, String message) {
Log.e("Security", "安全SDK初始化失败: " + message);
}
});
}

public static void reportGameData(String event, Map<String, Object> data) {
TssSdk.reportEvent(event, data);
}

public static boolean isCheating() {
return TssSdk.isCheatDetected();
}
}

性能优化与测试

Monkey压力测试

1
2
3
4
5
6
7
8
9
10
11
# 基础测试
adb shell monkey -p com.your.package 1000

# 详细测试
adb shell monkey -p com.your.package \
--throttle 300 \
--ignore-crashes \
--ignore-timeouts \
--ignore-security-exceptions \
--monitor-native-crashes \
10000

内存优化

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
public class MemoryOptimizer {

public static Bitmap optimizeBitmap(Bitmap source, int maxWidth, int maxHeight) {
int width = source.getWidth();
int height = source.getHeight();

float scale = Math.min(
(float) maxWidth / width,
(float) maxHeight / height
);

if (scale < 1) {
int newWidth = (int) (width * scale);
int newHeight = (int) (height * scale);
return Bitmap.createScaledBitmap(source, newWidth, newHeight, true);
}

return source;
}

public static void recycleBitmap(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}

小结

这篇文章记录了Android游戏开发中遇到的实际问题和解决方案,包括签名问题、线程处理、防沉迷合规、防外挂等。这些都是实际项目中的踩坑经验,希望能帮到你。


有问题欢迎交流。