问题8. 网络优化
- 优化方案:
- 请求合并
- 缓存策略
- 断点续传
- 图片压缩
- 协议优化
详细讲解
1. 网络优化的核心问题 🎯
想象一下,网络请求就像在餐厅点餐:
- 客户端(手机)是顾客
- 服务器是厨房
- 网络连接是服务员
2. 主要优化方向 🔧
2.1 DNS优化
就像顾客要知道餐厅具体位置:
- DNS预解析:提前记住常去餐厅的地址
- DNS缓存:把常用地址记在本地备忘录里
- HttpDNS:直接问知情人要地址,不用查地图(DNS服务器)
2.2 连接复用
类比:
- 不用每次点菜都换服务员
- 保持长连接(Keep-Alive)
- 连接池:预备几个固定服务员
2.3 请求优化
就像点菜策略:
- 合并请求:一次性把所有菜品都点完
- 请求压缩:把长点单简化成套餐代号
- 请求优先级:重要的菜先点
2.4 数据传输优化
相当于:
- 压缩:把大份菜打包得更紧凑
- 缓存:常点的菜记住配方
- 断点续传:上次没吃完的可以接着吃
3. 网络优化流程图
graph TD
A[App发起请求] --> B{是否有DNS缓存?}
B -->|是| D[使用缓存DNS]
B -->|否| C[DNS解析]
C --> D
D --> E{是否有可用连接?}
E -->|是| G[复用连接]
E -->|否| F[建立新连接]
F --> G
G --> H[发送请求]
H --> I{是否有缓存?}
I -->|是| K[使用缓存]
I -->|否| J[网络传输]
J --> L[接收响应]
L --> M[缓存管理]
M --> N[请求完成]
4. 具体优化技术 🛠
- 协议层优化
- 使用HTTP/2.0
- 多路复用
- 头部压缩
- 缓存层优化
- 三级缓存(内存、硬盘、网络)
- 缓存策略(LRU等)
- 缓存有效期管理
- 传输层优化
- TCP参数优化
- 弱网优化
- 流量控制
5. 实践建议 💡
- 监控指标
class NetworkMonitor {
fun monitor() {
// DNS解析时间
// TCP连接时间
// 请求响应时间
// 流量消耗
}
}
- 优化示例
class NetworkOptimization {
// DNS预解析
fun preDns() {
OkHttpClient.Builder()
.dns(CustomDns())
.build()
}
// 连接池管理
fun connectionPool() {
OkHttpClient.Builder()
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
.build()
}
}
6. 效果评估 📊
优化效果主要体现在:
- 首次加载速度提升
- 弱网表现改善
- 流量消耗降低
- 电量消耗优化
记住一点:网络优化是一个持续的过程,需要根据实际监控数据不断调整优化策略。就像餐厅服务,需要不断根据顾客反馈来改进服务流程。
具体方案
好的,我来详细讲解每个网络优化方案的具体实现。
1. DNS 优化详解 🌐
1.1 传统 DNS 问题
graph LR
A[App] --> B[本地DNS解析器]
B --> C[运营商Local DNS]
C --> D[权威DNS服务器]
D --> C
C --> B
B --> A
1.2 HttpDNS 实现
class HttpDnsManager {
// 预解析域名列表
private val preResolveDomains = listOf(
"api.example.com",
"img.example.com"
)
// HttpDNS实现
fun httpDnsResolve(domain: String): String {
return try {
// 1. 先查本地缓存
val ipFromCache = DnsCache.get(domain)
if (ipFromCache != null) return ipFromCache
// 2. 请求HttpDNS服务
val response = HttpClient.get("https://httpdns.com/resolve?domain=$domain")
// 3. 缓存结果
DnsCache.put(domain, response.ip, response.ttl)
response.ip
} catch (e: Exception) {
// 4. 降级使用本地DNS
System.getDefault().lookup(domain)
}
}
}
2. 连接复用优化 🔄
2.1 连接池管理
class ConnectionPoolManager {
private val connectionPool = ConnectionPool(
maxIdleConnections = 5, // 最大空闲连接数
keepAliveDuration = 5, // 连接保持时间
timeUnit = TimeUnit.MINUTES
)
fun initOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectionPool(connectionPool)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build()
}
}
2.2 长连接管理
class WebSocketManager {
private var webSocket: WebSocket? = null
fun initWebSocket() {
val request = Request.Builder()
.url("ws://example.com/ws")
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// 连接建立
startHeartbeat()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
// 自动重连
retryConnection()
}
})
}
private fun startHeartbeat() {
// 心跳保活
scope.launch {
while (isActive) {
webSocket?.send("ping")
delay(30_000) // 30秒
}
}
}
}
3. 请求优化 📡
3.1 请求合并
class RequestMerger {
private val requestQueue = ConcurrentLinkedQueue<Request>()
private val mergeThreshold = 100L // 合并时间窗口
fun addRequest(request: Request) {
requestQueue.offer(request)
checkAndMerge()
}
private fun checkAndMerge() {
if (requestQueue.size >= 3 || timeExceeded()) {
val requests = requestQueue.toList()
requestQueue.clear()
// 合并请求
val mergedRequest = MergedRequest.Builder()
.addAll(requests)
.build()
executeRequest(mergedRequest)
}
}
}
3.2 请求压缩
class CompressionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// 添加压缩请求头
val compressedRequest = originalRequest.newBuilder()
.header("Accept-Encoding", "gzip")
.build()
val response = chain.proceed(compressedRequest)
// 处理压缩响应
return if (response.header("Content-Encoding") == "gzip") {
decompressResponse(response)
} else {
response
}
}
}
4. 缓存优化 💾
4.1 三级缓存实现
class CacheManager {
// 内存缓存
private val memoryCache = LruCache<String, CacheData>(
maxSize = 10 * 1024 * 1024 // 10MB
)
// 磁盘缓存
private val diskCache = DiskLruCache.open(
directory = cacheDir,
maxSize = 50 * 1024 * 1024 // 50MB
)
suspend fun getData(url: String): Result<Data> {
// 1. 查内存缓存
memoryCache.get(url)?.let { return Result.success(it) }
// 2. 查磁盘缓存
diskCache.get(url)?.let {
// 同时放入内存缓存
memoryCache.put(url, it)
return Result.success(it)
}
// 3. 网络请求
return try {
val response = networkFetch(url)
// 存入缓存
saveToCache(url, response)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
}
4.2 缓存策略
sealed class CacheStrategy {
// 仅网络
object NetworkOnly : CacheStrategy()
// 仅缓存
object CacheOnly : CacheStrategy()
// 优先缓存
object CacheFirst : CacheStrategy()
// 优先网络
object NetworkFirst : CacheStrategy()
// 同时请求
object CacheAndNetwork : CacheStrategy()
}
class CacheInterceptor(
private val strategy: CacheStrategy
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return when (strategy) {
is CacheFirst -> handleCacheFirst(chain)
is NetworkFirst -> handleNetworkFirst(chain)
// ... 其他策略处理
}
}
}
5. 弱网优化 📶
5.1 请求重试
class RetryInterceptor(
private val maxRetries: Int = 3
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var retryCount = 0
var response: Response? = null
while (retryCount < maxRetries) {
try {
response = chain.proceed(chain.request())
if (response.isSuccessful) return response
retryCount++
} catch (e: IOException) {
if (retryCount == maxRetries - 1) throw e
retryCount++
// 指数退避
Thread.sleep((2.0.pow(retryCount.toDouble()) * 1000).toLong())
}
}
return response ?: throw IOException("Retry failed")
}
}
5.2 断点续传
class DownloadManager {
fun download(url: String, file: File) {
// 获取已下载长度
val downloadedLength = if (file.exists()) file.length() else 0
val request = Request.Builder()
.url(url)
.header("Range", "bytes=$downloadedLength-")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
// 续传下载
response.body?.let { body ->
file.appendBytes(body.bytes())
}
}
})
}
}
6. 监控系统 📊
class NetworkMonitor {
data class Metrics(
val dnsTime: Long,
val connectTime: Long,
val requestTime: Long,
val responseTime: Long,
val totalTime: Long,
val bytesSent: Long,
val bytesReceived: Long
)
fun addNetworkInterceptor(): Interceptor {
return Interceptor { chain ->
val startTime = System.nanoTime()
val request = chain.request()
val response = chain.proceed(request)
val metrics = Metrics(
dnsTime = calculateDnsTime(),
connectTime = calculateConnectTime(),
// ... 其他指标计算
)
// 上报指标
reportMetrics(metrics)
response
}
}
}
实践建议 💡
-
分阶段实施
- 先实现基础优化(DNS、连接池)
- 再添加高级特性(缓存、压缩)
- 最后是监控和调优
-
监控指标
- 请求成功率
- 响应时间分布
- 流量消耗
- 缓存命中率
-
降级策略
- 网络请求失败时的降级方案
- 弱网环境下的业务降级
- 服务器压力大时的请求限流
通过以上这些优化方案的组合实施,可以显著提升应用的网络性能。记住要根据实际监控数据来评估每个优化方案的效果,选择最适合你的应用场景的优化策略。
问题9. Bitmap加载优化
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
详细讲解
我来为您生动形象地讲解 Bitmap 加载优化的底层原理。
1. 基本概念 🖼️
Bitmap(位图)在内存中的占用计算公式:
占用内存 = 图片宽度 × 图片高度 × 每个像素占用的字节数
例如一张 2000×1000 像素的 ARGB_8888 格式图片:
内存占用 = 2000 × 1000 × 4 bytes = 8MB
2. 主要优化方向 🎯
2.1 采样率压缩 (inSampleSize)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 只获取图片信息,不加载到内存
BitmapFactory.decodeFile(imagePath, options);
// 计算采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false; // 真正加载图片
2.2 像素格式压缩
BitmapFactory.Options options = new BitmapFactory.Options();
// ARGB_8888 (4字节/像素) -> RGB_565 (2字节/像素)
options.inPreferredConfig = Bitmap.Config.RGB_565;
3. 完整优化流程图 📊
graph TD
A[原始图片] --> B{是否需要压缩?}
B -->|是| C[计算采样率]
C --> D[设置像素格式]
D --> E[加载图片]
B -->|否| E
E --> F[内存缓存]
F --> G[显示图片]
4. 具体优化策略 💡
- 内存缓存
// LruCache 实现
private LruCache<String, Bitmap> mMemoryCache;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
- 磁盘缓存
DiskLruCache mDiskLruCache;
File cacheDir = getDiskCacheDir(context, "bitmap");
mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024); // 10MB
5. 最佳实践建议 📝
- 按需加载
- 列表滑动时暂停加载
- 仅加载可见区域图片
- 分级缓存
内存缓存(速度快) -> 磁盘缓存(速度中) -> 网络加载(速度慢)
- 及时回收
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
setImageBitmap(null);
}
6. 注意事项 ⚠️
- 大图片处理
- 先获取图片实际尺寸
- 根据显示区域计算采样率
- 分块加载超大图片
- 内存泄漏防护
- 使用弱引用
- Activity销毁时及时清理
- 避免静态引用持有Context
总结 🎉
Bitmap优化是一个系统工程,需要:
- 合理的采样压缩
- 高效的缓存策略
- 及时的内存回收
- 优化的加载时机
通过以上措施可以显著提升应用性能和用户体验。
问题10. Native Crash 分析定位
-
分析方法:
- 收集崩溃日志(.so库地址、发生崩溃的代码行等)
- 通过 ndk-stack 工具解析日志
- 使用 addr2line 定位具体代码行
- 结合 tombstone 文件分析
-
解决流程:
graph TD
A[收集Crash信息] --> B[符号表解析]
B --> C[定位代码位置]
C --> D[分析调用栈]
D --> E[复现与修复]
详细讲解
我来为您详细讲解 Native Crash 分析定位的原理和方法。
一、什么是 Native Crash?
Native Crash 是指在 Android 应用程序的 Native 层(C/C++ 代码)发生的崩溃。当程序在执行 Native 代码时发生严重错误,比如:
- 空指针访问
- 内存访问越界
- 栈溢出
- 非法指令
二、Crash 信息收集原理
graph TD
A[程序运行] --> B[发生 Native Crash]
B --> C[信号处理机制]
C --> D[信号处理函数]
D --> E[生成 crash dump]
E --> F[收集设备信息]
F --> G[保存崩溃日志]
- 信号捕获机制
// 注册信号处理函数
struct sigaction act;
act.sa_sigaction = crash_handler; // 自定义处理函数
act.sa_flags = SA_SIGINFO; // 携带详细信息
sigaction(SIGSEGV, &act, NULL); // 注册SIGSEGV信号处理
- 关键信息收集
- 崩溃时的调用栈(stack trace)
- 寄存器状态
- 内存映射信息
- 设备信息
三、分析定位流程
- 符号化处理
# 使用 addr2line 工具进行符号化
addr2line -e libxxx.so -f -C 0xabcd
- 日志分析要点
- Crash 发生的具体位置(文件名、行号)
- 崩溃时的调用栈信息
- 寄存器状态分析
- 内存映射分析
四、常见分析工具
- ndk-stack
adb logcat | ndk-stack -sym $PROJECT_PATH/obj/local/armeabi-v7a/
- addr2line
- gdb
- lldb
五、实战分析步骤
- 收集崩溃日志
# 导出崩溃日志
adb logcat > crash.log
- 提取关键信息
- 查找 "Fatal signal" 相关日志
- 定位 "backtrace" 信息
- 检查 "maps" 内存映射
- 符号化解析
# 使用 ndk-stack 解析
cat crash.log | ndk-stack -sym ./obj/local/armeabi-v7a/
六、预防措施
- 代码层面
- 内存访问检查
- 指针使用规范
- 边界检查
- 工具层面
- AddressSanitizer (ASAN)
- Valgrind
- Native Debug
总结流程图
graph TB
A[Native Crash 发生] --> B[信号捕获]
B --> C[收集信息]
C --> D[生成日志]
D --> E[符号化处理]
E --> F[分析定位]
F --> G[修复问题]
subgraph 信息收集
C1[调用栈] --> C
C2[寄存器状态] --> C
C3[内存映射] --> C
end
subgraph 分析工具
E1[ndk-stack] --> E
E2[addr2line] --> E
E3[gdb/lldb] --> E
end
实用建议
- 建立完整的符号表管理系统
- 规范化的日志收集机制
- 自动化的分析工具链
- 建立 Crash 问题知识库
通过以上方法和工具的组合使用,我们可以更有效地定位和解决 Native Crash 问题。关键是要建立起完整的工具链和分析流程,确保问题能够被快速定位和解决。
具体线上方案
让我为您详细介绍线上 Native Crash 抓取的方案。
一、常用抓取方案
graph LR
A[线上Crash收集] --> B[即时上报]
A --> C[本地存储]
B --> D[崩溃分析平台]
C --> D
D --> E[问题分析]
二、主要实现方式
- 集成第三方平台
// 常见平台初始化示例
public class CrashManager {
public static void init(Context context) {
// Bugly
CrashReport.initCrashReport(context, "APP_ID", false);
// Firebase Crashlytics
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true);
}
}
- 自研崩溃收集系统
// Native层信号处理
void setupNativeCrashHandler() {
struct sigaction act;
act.sa_sigaction = crashHandler;
act.sa_flags = SA_SIGINFO;
// 注册关键信号
sigaction(SIGSEGV, &act, nullptr);
sigaction(SIGABRT, &act, nullptr);
sigaction(SIGFPE, &act, nullptr);
// ... 其他信号
}
// 崩溃处理函数
void crashHandler(int sig, siginfo_t* info, void* context) {
// 1. 收集设备信息
// 2. 获取调用栈
// 3. 保存现场信息
// 4. 异步上报或本地存储
}
三、关键技术点
- Dump文件生成
void generateDumpFile() {
// 1. 收集进程信息
char maps_path[64];
snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", getpid());
// 2. 收集内存信息
// 3. 保存寄存器状态
// 4. 记录调用栈
}
- 本地存储策略
public class CrashStorage {
// 存储限制
private static final int MAX_CRASH_FILES = 5;
private static final long MAX_STORAGE_SIZE = 10 * 1024 * 1024; // 10MB
public void saveCrashLog(String crashLog) {
// 1. 检查存储空间
// 2. 清理旧文件
// 3. 写入新日志
}
}
四、上报策略
- 分级上报
public class CrashReporter {
public void report(CrashInfo crashInfo) {
switch (crashInfo.getSeverity()) {
case HIGH:
// 立即上报
reportImmediately(crashInfo);
break;
case MEDIUM:
// WiFi环境上报
reportWhenWifi(crashInfo);
break;
case LOW:
// 延迟上报
addToReportQueue(crashInfo);
break;
}
}
}
- 网络策略
public class ReportStrategy {
public void uploadCrashLog() {
// 1. 检查网络状态
if (!isWifiConnected() && !isAllowedOnMobile()) {
return;
}
// 2. 压缩日志
// 3. 分片上传
// 4. 失败重试
}
}
五、最佳实践
- 合理的采样率
public class SamplingConfig {
// 按用户采样
private static boolean shouldSampleUser() {
return getUserId().hashCode() % 100 < SAMPLING_RATE;
}
// 按设备采样
private static boolean shouldSampleDevice() {
return getDeviceId().hashCode() % 100 < DEVICE_SAMPLING_RATE;
}
}
- 数据脱敏
public class DataMasker {
public static String maskSensitiveInfo(String crashLog) {
// 脱敏手机号
crashLog = crashLog.replaceAll("1\\d{10}", "1****");
// 脱敏邮箱
crashLog = crashLog.replaceAll("[\\w.-]+@[\\w.-]+", "****@****");
return crashLog;
}
}
六、监控告警
graph TD
A[Crash发生] --> B[数据收集]
B --> C[实时监控]
C --> D{是否触发阈值}
D -->|是| E[告警通知]
D -->|否| F[常规处理]
E --> G[紧急处理]
七、注意事项
- 性能影响
- 采用异步写入
- 控制日志大小
- 合理设置采样率
- 稳定性保证
- 崩溃收集模块本身的稳定性
- 防止循环崩溃
- 存储空间管理
- 隐私合规
- 用户授权
- 数据脱敏
- 合规存储
八、完整方案示例
public class CrashSolution {
public static void init(Context context) {
// 1. 初始化配置
initConfig();
// 2. 注册Native崩溃处理
NativeCrashHandler.register();
// 3. 注册Java崩溃处理
JavaCrashHandler.register();
// 4. 初始化存储
CrashStorage.init(context);
// 5. 初始化上报
CrashReporter.init();
}
}
通过以上方案,可以建立一个完整的线上 Native Crash 收集系统。关键是要在稳定性、性能、合规性之间找到平衡点,同时确保数据的及时性和完整性。
问题11. 代码混淆步骤
- 配置过程:
android {
buildTypes {
release {
// 启用混淆
minifyEnabled true
// 启用资源压缩
shrinkResources true
// 指定混淆规则文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
- 主要步骤:
- 添加混淆规则配置文件
- 保留需要的类和方法
- 配置第三方库混淆规则
- 测试混淆后的应用
- 保存混淆映射文件
详细讲解
我来为您生动形象地讲解代码混淆的底层原理和具体实现方式。
代码混淆的本质
代码混淆实际上就像是把一本写得很清晰的书重新改写成一本内容相同但难以阅读的书,但保证这本书依然能够正常使用。
主要混淆步骤和原理
- 命名混淆 🏷️
// 原始代码
public class UserService {
private String userName;
public void processUserData() {
System.out.println(userName);
}
}
// 混淆后
public class a {
private String b;
public void c() {
System.out.println(b);
}
}
就像把所有人的名字都改成无意义的代号,但保证每个人知道自己的新代号。
- 控制流混淆 🔄
// 原始代码
if (condition) {
doSomething();
} else {
doOtherThing();
}
// 混淆后
int x = condition ? 1 : 2;
switch(x) {
case 1: doSomething(); break;
case 2: doOtherThing(); break;
}
相当于把直接的路径改成迂回路线,最终到达相同目的地。
- 字符串加密 🔐
// 原始代码
String message = "Hello World";
// 混淆后
String message = new String(new byte[]{72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100});
就像把明文改写成密码,但程序运行时能自动解密。
- 代码重排序 📑
// 原始代码
void process() {
step1();
step2();
step3();
}
// 混淆后
void process() {
if(true) {
step2();
if(1 < 2) {
step1();
step3();
}
}
}
像打乱书的章节顺序,但保持逻辑关系不变。
混淆流程图
graph TD
A[源代码] --> B[解析AST语法树]
B --> C[命名混淆]
C --> D[控制流混淆]
D --> E[字符串加密]
E --> F[代码重排序]
F --> G[生成混淆后代码]
G --> H[验证功能正确性]
具体实现要点
- 预处理阶段 📝
- 分析代码依赖关系
- 识别不能混淆的关键部分(如API接口)
- 建立符号表
- 混淆处理阶段 🔄
- 生成混淆映射表
- 应用各种混淆规则
- 保持代码语义不变
- 后处理阶段 ✅
- 优化混淆后的代码
- 验证功能完整性
- 生成混淆映射文件
注意事项 ⚠️
- 需要保留的内容:
- 入口点方法名
- 接口定义
- 反射使用的类名
- Native方法名
- 常见陷阱:
- 反射调用可能失败
- 序列化可能出错
- 动态加载可能失败
实用建议 💡
- 分层混淆
- 对不同重要程度的代码采用不同强度的混淆
- 核心算法采用更强的混淆策略
- 保护措施
- 添加反调试代码
- 加入完整性校验
- 使用代码加密技术
通过以上方式,代码混淆能够有效提高逆向工程的难度,保护知识产权。记住,好的混淆方案应该在保护强度和运行效率之间找到平衡点。
具体配置
我来详细讲解 Android 混淆的具体配置方法。
一、基础配置位置 📝
混淆配置主要在以下文件中:
android {
buildTypes {
release {
minifyEnabled true // 启用混淆
shrinkResources true // 启用资源压缩
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
二、常用混淆规则详解 🔍
- 基础配置
# 指定代码的压缩级别
-optimizationpasses 5
# 不跳过非公共库的类成员
-dontskipnonpubliclibraryclassmembers
# 混淆时采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
# 保持源文件和行号
-keepattributes SourceFile,LineNumberTable
- 保持四大组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
- 保持自定义View
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(...);
public * get*();
}
- 保持实体类
# 保持所有实体类
-keep class com.your.package.model.** { *; }
# 保持含有Parcelable接口的类
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保持Serializable接口的类
-keep class * implements java.io.Serializable { *; }
- 第三方库混淆配置示例
# Retrofit
-keepattributes Signature
-keepattributes Exceptions
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
# Gson
-keep class com.google.gson.** { *; }
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
三、常见特殊情况处理 ⚠️
- WebView 相关
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
- JS 接口
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
- 枚举类
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
四、检查混淆效果 🔍
- 查看混淆映射 混淆后会在以下位置生成映射文件:
app/build/outputs/mapping/release/mapping.txt
- 使用工具还原混淆堆栈
./retrace.sh mapping.txt obfuscated_stack_trace.txt
五、实用建议 💡
- 分模块配置
# 每个模块单独配置
-keep class com.your.package.module1.** { *; }
-keep class com.your.package.module2.api.** { *; }
- 调试配置
# 调试时保留更多信息
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
- 常见问题解决方案
# 解决反射问题
-keepclasseswithmembers class * {
native <methods>;
}
# 保持注解
-keepattributes *Annotation*
六、混淆检查清单 ✅
- 检查以下内容是否正确配置:
- 四大组件
- 自定义View
- 序列化对象
- 第三方库
- WebView相关
- 反射用到的类
- JNI调用的方法
- 测试检查项:
- 基本功能是否正常
- 崩溃日志是否可读
- 第三方SDK是否正常
- 网络请求是否正常
- 数据存储是否正常
七、优化建议 🚀
- 资源优化
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}
- 保持关键日志
-keepclassmembers class * {
void debug(...);
void error(...);
}
记住:
- 每次添加新库都要检查其混淆规则
- 定期检查混淆后的应用功能
- 保存每个版本的 mapping 文件
- 测试所有功能点,特别是涉及反射的部分
问题12. App 电量优化
-
优化方案:
- 优化网络请求频率
- 合理使用定位服务
- 避免后台无效唤醒
- 使用 WorkManager 调度任务
- 优化 WakeLock 使用
-
监控方案:
graph LR
A[电量优化] --> B[定位使用优化]
A --> C[网络请求优化]
A --> D[后台任务优化]
A --> E[唤醒锁优化]
具体方案
我来为您生动形象地讲解 Android 电量优化的底层原理和具体实现方式。
🔋 Android 电量消耗的主要来源
-
CPU 处理
- 频繁的计算
- 后台任务
- Wake Lock 持锁
-
网络通信
- 频繁的网络请求
- 长连接维持
- 数据同步
-
定位服务
- GPS 定位
- 网络定位
- 持续的位置更新
-
屏幕显示
- 高亮度
- 动画效果
- 刷新频率
📱 优化方案及原理
1. Doze 模式优化
graph TD
A[设备静止] --> B[屏幕关闭]
B --> C[电池供电]
C --> D[进入 Doze 模式]
D --> E[限制网络访问]
D --> F[暂停后台任务]
D --> G[推迟同步]
具体实现:
class PowerManager {
fun handleDozeMode() {
// 检查是否支持 Doze
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
// 判断是否忽略电池优化
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
// 申请忽略电池优化权限
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
}
}
}
}
2. WorkManager 任务调度
graph LR
A[任务创建] --> B[任务约束]
B --> C[系统调度]
C --> D[批量执行]
D --> E[省电执行]
示例代码:
class BackgroundTaskManager {
fun scheduleTask() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(false)
.build()
val workRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES, // 最小间隔 15 分钟
5, TimeUnit.MINUTES // 灵活时间窗口
).setConstraints(constraints)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"sync_task",
ExistingPeriodicWorkPolicy.KEEP,
workRequest
)
}
}
3. 网络优化
- 批量请求:将多个请求合并
- 预取数据:在合适时机预先加载
- 压缩数据:减少传输量
class NetworkManager {
fun optimizeNetworkRequests() {
// 实现批量请求
val batchRequest = BatchRequestBuilder()
.addRequest(request1)
.addRequest(request2)
.setExecutionWindow(30_000) // 30秒内的请求批量处理
.build()
// 实现预取
prefetchManager.prefetchData(
when = {
isWifiConnected && batteryLevel > 30
}
)
}
}
📊 性能监控
-
Battery Historian 分析
- 收集电量使用数据
- 分析耗电来源
- 优化关键点
-
Android Vitals 监控
- ANR 监控
- 崩溃监控
- 耗电监控
🎯 最佳实践总结
-
延迟批处理
- 使用 WorkManager 调度任务
- 合并网络请求
- 适应系统 Doze 模式
-
资源按需使用
- 及时释放 WakeLock
- 按需使用定位服务
- 合理使用传感器
-
网络优化
- 减少请求频率
- 压缩传输数据
- 使用缓存策略
-
UI 优化
- 减少动画使用
- 避免过度绘制
- 优化布局层级
这些优化措施综合起来,能够显著提升应用的电量效率。关键是要根据应用的具体场景,选择合适的优化策略进行实施。
问题13.大图加载
- 处理方案:
public static Bitmap handleLargeBitmap(String path, int reqWidth, int reqHeight) {
// 第一次解码获取尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
// 计算采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 实际解码
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
}
- 优化策略:
- 图片压缩
- 内存缓存
- 磁盘缓存
- 分级加载
- 及时回收
具体方案
我来用通俗易懂的方式讲解 Android 大图加载的底层原理。
🌟 基本问题
想象你要在一个小房间(手机内存)里展示一张巨大的照片(高清图片)。直接把整张照片搬进去显然会撑爆房间,所以我们需要想办法处理这个问题。
📱 加载大图的主要步骤
- 采样率计算 (BitmapFactory.Options)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 先只获取图片尺寸,不加载到内存
BitmapFactory.decodeFile(imagePath, options);
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
int sampleSize = calculateInSampleSize(imageWidth, imageHeight, reqWidth, reqHeight);
- 内存复用 (BitmapPool)
// 复用已有的Bitmap内存
options.inBitmap = bitmapPool.getBitmap();
options.inMutable = true;
🔄 具体加载流程
-
预处理阶段
- 获取图片实际尺寸
- 计算采样率
- 准备复用内存
-
加载阶段
- 根据采样率加载图片
- 使用 BitmapRegionDecoder 分块加载
- 内存复用减少抖动
-
显示阶段
- 图片缓存
- 生命周期管理
🎯 核心技术要点
- 采样率计算
private int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) {
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
- 分块加载
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream, false);
Rect region = new Rect(0, 0, width, height);
Bitmap bitmap = decoder.decodeRegion(region, options);
📊 流程图
graph TD
A[原始图片] --> B[计算采样率]
B --> C[内存复用准备]
C --> D{是否需要分块?}
D -->|是| E[分块加载]
D -->|否| F[直接加载]
E --> G[显示到界面]
F --> G
💡 优化建议
-
内存优化
- 使用 RGB_565 替代 ARGB_8888
- 及时回收不用的 Bitmap
- 使用 BitmapPool 复用内存
-
加载优化
- 异步加载
- 分级缓存(内存、磁盘)
- 预加载机制
🌰 实际应用示例
class LargeImageLoader {
fun loadLargeImage(context: Context, imageUrl: String, imageView: ImageView) {
// 异步加载
coroutineScope.launch(Dispatchers.IO) {
// 1. 计算采样率
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
// 2. 设置复用
options.apply {
inSampleSize = calculateSampleSize()
inMutable = true
inBitmap = bitmapPool.getBitmap()
}
// 3. 加载图片
val bitmap = BitmapFactory.decodeFile(imageUrl, options)
// 4. 显示图片
withContext(Dispatchers.Main) {
imageView.setImageBitmap(bitmap)
}
}
}
}
📝 总结
大图加载的核心就是:
- 合理的采样率计算
- 高效的内存复用
- 智能的分块加载
- 完善的缓存机制
通过这些技术的组合使用,我们可以在有限的移动设备内存中流畅地加载和显示大图,同时保证应用的稳定性和性能。
总结
性能优化是一个系统工程,需要从多个维度进行:
- 内存优化
- 布局优化
- 启动优化
- 网络优化
- 电量优化
- 包体积优化
建议使用工具如Android Profiler、LeakCanary等进行监控和分析,根据实际情况选择合适的优化方案