面试题 - Android - 性能优化(一)

127 阅读18分钟

问题1. 内存优化相关

内存泄漏的常见情况及解决方案:

  1. 常见场景

    • 静态变量持有Activity引用
    • 单例模式持有Context
    • Handler内部类引用
    • 线程/AsyncTask未正确关闭
    • 注册监听器未注销
  2. 解决方案

    • 使用WeakReference
    • 在Activity销毁时及时释放资源
    • 使用Application Context
    • 避免静态变量持有Activity引用

Handler避免内存泄漏:

public class MyActivity extends Activity {
    private static class MyHandler extends Handler {
        private WeakReference<MyActivity> activityRef;
        
        MyHandler(MyActivity activity) {
            activityRef = new WeakReference<>(activity);
        }
        
        @Override
        public void handleMessage(Message msg) {
            MyActivity activity = activityRef.get();
            if (activity != null) {
                // 处理消息
            }
        }
    }
}

OOM问题解决:

  1. 产生原因
    • 加载大图片
    • 内存泄漏累积
    • 数据结构过大
  2. 解决方案
    • 图片压缩
    • 及时释放资源
    • 使用内存缓存
    • 分批加载数据

详细解释

1.泄漏实例分析

1 静态变量持有Activity引用

❌ 错误示例:

public class MainActivity extends Activity {
    // 错误方式:静态变量持有Activity引用
    private static TextView textView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        textView = new TextView(this);
    }
}

✅ 正确示例:

public class MainActivity extends Activity {
    // 正确方式:使用WeakReference
    private static WeakReference<TextView> textViewRef;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        textViewRef = new WeakReference<>(new TextView(this));
    }
}
1.2 单例模式内存泄漏

❌ 错误示例:

public class Manager {
    private static Manager instance;
    private Context context;
    
    private Manager(Context context) {
        this.context = context;
    }
    
    public static Manager getInstance(Context context) {
        if (instance == null) {
            instance = new Manager(context);
        }
        return instance;
    }
}

✅ 正确示例:

public class Manager {
    private static Manager instance;
    private Context applicationContext;
    
    private Manager(Context context) {
        // 使用ApplicationContext而不是Activity的Context
        this.applicationContext = context.getApplicationContext();
    }
    
    public static Manager getInstance(Context context) {
        if (instance == null) {
            instance = new Manager(context);
        }
        return instance;
    }
}
1.3 匿名内部类/Handler导致的泄漏

❌ 错误示例:

public class MainActivity extends Activity {
    // 错误方式:非静态Handler导致内存泄漏
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 处理消息
        }
    };
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handler.sendMessageDelayed(Message.obtain(), 10000);
    }
}

✅ 正确示例:

public class MainActivity extends Activity {
    // 正确方式:使用静态内部类和弱引用
    private static class MyHandler extends Handler {
        private WeakReference<MainActivity> activityRef;
        
        MyHandler(MainActivity activity) {
            activityRef = new WeakReference<>(activity);
        }
        
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityRef.get();
            if (activity != null) {
                // 处理消息
            }
        }
    }
    
    private final MyHandler handler = new MyHandler(this);
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
    }
}
1.4 监听器未注销

❌ 错误示例:

public class MainActivity extends Activity {
    private LocationManager locationManager;
    private LocationListener locationListener;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        locationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                // 处理位置更新
            }
            // ... 其他回调方法
        };
        
        locationManager.requestLocationUpdates(
            LocationManager.GPS_PROVIDER, 
            0, 0, 
            locationListener
        );
    }
}

✅ 正确示例:

public class MainActivity extends Activity {
    private LocationManager locationManager;
    private LocationListener locationListener;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        locationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                // 处理位置更新
            }
            // ... 其他回调方法
        };
        
        locationManager.requestLocationUpdates(
            LocationManager.GPS_PROVIDER, 
            0, 0, 
            locationListener
        );
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 及时注销监听器
        if (locationManager != null && locationListener != null) {
            locationManager.removeUpdates(locationListener);
        }
    }
}

2. 内存优化工具使用

2.1 使用LeakCanary检测内存泄漏
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.x'
}
2.2 使用Android Profiler分析内存
graph TD
    A[打开Android Profiler] --> B[选择Memory分析]
    B --> C[强制GC]
    C --> D[查看内存分配]
    D --> E[堆内存快照]
    E --> F[分析对象引用]

3. Bitmap内存优化实例

public class ImageUtils {
    public static Bitmap decodeSampledBitmap(String filePath, int reqWidth, int reqHeight) {
        // 第一次解码,只获取图片尺寸
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(filePath, options);

        // 计算压缩比例
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // 实际解码图片
        options.inJustDecodeBounds = false;
        // 使用RGB_565比ARGB_8888节省一半内存
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        return BitmapFactory.decodeFile(filePath, options);
    }

    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}
内存优化核心要点:
  1. 及时释放资源

    • Activity销毁时清理资源
    • 取消异步任务
    • 注销监听器
    • 关闭IO流
  2. 合理使用缓存

    • 使用LruCache缓存Bitmap
    • 避免重复创建对象
    • 复用系统资源
  3. 避免内存抖动

    • 避免在循环中创建对象
    • 使用对象池
    • 减少临时对象创建
  4. 图片优化

    • 按需加载
    • 合理压缩
    • 及时回收

通过以上具体例子和实践,我们可以更好地理解和处理Android中的内存优化问题。记住,内存优化是一个持续的过程,需要在开发过程中时刻注意并养成良好的编码习惯。

问题2. ANR优化

ANR出现场景及解决:

graph TD
    A[ANR类型] --> B[主线程阻塞]
    A --> C[BroadcastReceiver超时]
    A --> D[Service超时]
    B --> E[耗时操作放入子线程]
    C --> F[避免在广播中做耗时操作]
    D --> G[Service及时停止]

详细解释

让我详细解释 ANR (Application Not Responding) 的底层原理:

一、什么是 ANR?

ANR 就像是手机给应用程序打分,如果应用在规定时间内没有完成特定任务,就会被判定为"不及格"。具体时间限制如下:

  • 输入事件(按键、触摸):5秒
  • 广播接收器:10秒(前台)/ 60秒(后台)
  • 服务:20秒

二、ANR 的底层监测原理

graph TD
    A[AMS发送消息] --> B[主线程Handler接收]
    B --> C[执行任务]
    C --> D{是否超时?}
    D -->|是| E[触发ANR]
    D -->|否| F[正常执行]
    E --> G[收集信息]
    G --> H[生成traces.txt]
    H --> I[显示ANR对话框]

三、详细工作流程

  1. 消息分发机制
// 简化的消息处理流程
public class MessageQueue {
    boolean next() {
        // 计算消息超时时间
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            // 等待消息
            nativePollOnce(ptr, nextPollTimeoutMillis);
        }
    }
}
  1. 监控流程
sequenceDiagram
    participant AMS as ActivityManagerService
    participant App as 应用进程
    participant WDT as 看门狗线程
    
    AMS->>App: 发送任务执行消息
    AMS->>WDT: 启动监控
    WDT->>WDT: 开始计时
    alt 正常情况
        App->>AMS: 任务完成响应
        AMS->>WDT: 停止监控
    else 超时情况
        WDT->>AMS: 报告超时
        AMS->>App: 收集进程信息
        AMS->>AMS: 生成ANR报告
    end

四、ANR 产生的具体原因

  1. 主线程阻塞
// 错误示例
public void onCreate() {
    // 主线程中进行耗时操作
    Thread.sleep(6000); // 这会导致ANR
}

// 正确示例
public void onCreate() {
    new Thread(() -> {
        // 耗时操作放在子线程
        Thread.sleep(6000);
        runOnUiThread(() -> {
            // 更新UI
        });
    }).start();
}
  1. 死锁情况
// 可能导致死锁的示例
public class DeadLockExample {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    public void method1() {
        synchronized(lock1) {
            synchronized(lock2) {
                // 处理逻辑
            }
        }
    }
    
    public void method2() {
        synchronized(lock2) {
            synchronized(lock1) {
                // 处理逻辑
            }
        }
    }
}

五、ANR 监测机制

graph TD
    A[应用启动] --> B[AMS开启监控]
    B --> C{检测响应时间}
    C -->|超时| D[触发ANR]
    C -->|正常| E[继续执行]
    D --> F[收集进程信息]
    F --> G[生成traces文件]
    G --> H[弹出ANR对话框]
    H --> I[用户选择]
    I -->|等待| J[继续等待响应]
    I -->|关闭| K[结束进程]

六、如何避免 ANR

  1. 代码优化建议

    • 使用异步操作
    • 避免主线程IO
    • 合理使用Handler
    • 优化广播接收器
    • 使用WorkManager处理后台任务
  2. 实践示例

// 使用AsyncTask处理耗时操作
private class MyAsyncTask extends AsyncTask<Void, Void, String> {
    @Override
    protected String doInBackground(Void... params) {
        // 耗时操作
        return result;
    }

    @Override
    protected void onPostExecute(String result) {
        // 更新UI
    }
}

七、ANR 问题排查

  1. 信息收集

    • traces.txt文件
    • logcat日志
    • CPU使用情况
    • 内存信息
  2. 分析步骤

graph LR
    A[收集日志] --> B[分析调用栈]
    B --> C[定位耗时操作]
    C --> D[复现问题]
    D --> E[优化代码]
    E --> F[验证修复]

八、总结

ANR 的产生是一个复杂的系统监控过程,理解其原理对于开发高质量的 Android 应用至关重要。记住以下关键点:

  1. 避免在主线程进行耗时操作
  2. 合理使用异步机制
  3. 及时响应系统消息
  4. 建立有效的监控机制
  5. 做好性能优化

通过合理的架构设计和代码实现,大多数 ANR 问题是可以预防的。当 ANR 发生时,要善于利用系统提供的工具和日志来分析和解决问题。

问题3. 布局优化

  1. 优化技巧
    • 使用ConstraintLayout
    • 减少布局层级
    • 避免过度绘制
    • 使用ViewStub
    • 合理使用include和merge标签

详细解释

让我详细讲解Android布局优化的底层原理:

一、View的绘制流程

graph TD
    A[measure测量] --> B[layout布局]
    B --> C[draw绘制]
    A1[onMeasure] --> A2[measureChild]
    A2 --> A3[setMeasuredDimension]
    B1[onLayout] --> B2[确定子View位置]
    C1[onDraw] --> C2[绘制背景]
    C2 --> C3[绘制内容]
    C3 --> C4[绘制子View]
    C4 --> C5[绘制装饰]

二、布局加载流程

graph LR
    A[XML布局文件] --> B[资源加载]
    B --> C[解析XML]
    C --> D[实例化View]
    D --> E[构建View树]
    E --> F[测量布局绘制]

三、布局优化原理详解

1. 过度绘制优化

想象一下叠纸片的过程:

  • 如果你叠5张纸,就要花5倍的时间
  • 手机绘制UI也是一样,每多绘制一层,耗时就多一份

解决方案:

<!-- 优化前 -->
<FrameLayout
    android:background="@color/white">
    <LinearLayout
        android:background="@color/gray">
        <TextView
            android:background="@color/blue"/>
    </LinearLayout>
</FrameLayout>

<!-- 优化后 -->
<FrameLayout>
    <LinearLayout>
        <TextView
            android:background="@color/blue"/>
    </LinearLayout>
</FrameLayout>
2. 布局层级优化

就像搭积木:

  • 积木叠得越高(层级越深),越容易倒(性能越差)
  • 积木摆得越平(层级越扁平),越稳定(性能越好)

优化示例:

<!-- 优化前 -->
<LinearLayout>
    <LinearLayout>
        <LinearLayout>
            <TextView/>
            <TextView/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

<!-- 优化后:使用ConstraintLayout -->
<ConstraintLayout>
    <TextView/>
    <TextView/>
</ConstraintLayout>
3. 布局加载优化

就像看书:

  • 传统方式:一次性看完整本书(一次性加载所有布局)
  • 优化方式:先看目录,需要时再看具体章节(按需加载)

代码示例:

// ViewStub延迟加载示例
public class MainActivity extends Activity {
    private ViewStub viewStub;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        viewStub = findViewById(R.id.viewstub);
        // 需要时才加载
        if(needShow) {
            viewStub.inflate();
        }
    }
}

四、布局优化具体方案

1. 使用高效的布局容器
graph TD
    A[布局容器选择] --> B[ConstraintLayout]
    A --> C[RelativeLayout]
    A --> D[LinearLayout]
    B --> E[性能最好]
    C --> F[中等性能]
    D --> G[简单场景]
2. 避免嵌套

想象成一个快递包裹:

  • 一个盒子套一个盒子,拆起来很麻烦(多层嵌套)
  • 所有东西平铺在一个盒子里,方便取用(扁平化)
3. Include优化

就像积木游戏中的"预制件":

  • 经常用到的积木组合做成一个整体
  • 需要时直接拿来用,省时省力
<!-- 共用的标题栏布局:title_bar.xml -->
<include
    android:id="@+id/title_bar"
    layout="@layout/title_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
4. Merge优化

如同去掉不必要的包装盒:

<!-- 优化前 -->
<LinearLayout>
    <include layout="@layout/heavy_layout"/>
</LinearLayout>

<!-- 优化后 -->
<merge>
    <!-- 布局内容 -->
</merge>

五、性能提升检测

  1. Layout Inspector:查看视图层级
  2. GPU过度绘制:查看重叠区域
  3. Systrace:分析布局耗时

六、最佳实践总结

graph TD
    A[布局优化] --> B[降低层级]
    A --> C[减少测量]
    A --> D[避免过度绘制]
    B --> E[使用ConstraintLayout]
    C --> F[使用ViewStub]
    D --> G[移除多余背景]

记住一个简单的原则:

  • 就像整理房间一样
  • 不要堆积太多层(降低层级)
  • 不要重复做同样的事(避免过度绘制)
  • 需要时再拿(按需加载)
  • 常用的东西放在容易拿到的地方(合理使用include)

通过这些优化,可以显著提升应用的性能和响应速度。在实际开发中,应该根据具体场景选择合适的优化方案。

问题4. 图片优化

  1. 优化方案
    • 合理的图片压缩
    • 图片缓存
    • 根据分辨率加载不同尺寸
    • 使用WebP格式
    • 图片复用

详细解释

让我详细讲解图片优化的底层原理:

一、图片加载基本原理

graph TD
    A[图片文件] --> B[解码Decode]
    B --> C[压缩Compress]
    C --> D[缓存Cache]
    D --> E[显示Display]
    
    B1[占用内存 = 图片宽 x 高 x 每个像素占用字节数] --> B

二、图片解码原理

  1. 位图的内存占用
// 示例:一张1920x1080的ARGB_8888格式图片
int memory = 1920 * 1080 * 4    // 4是因为ARGB每个通道占8位,共32位,即4字节
// 大约占用7.9MB内存
  1. 色彩格式对比
  • ARGB_8888: 32位,4字节,最高质量
  • RGB_565: 16位,2字节,中等质量
  • ALPHA_8: 8位,1字节,仅存储透明度

三、图片压缩原理

1. 质量压缩
// 质量压缩不改变图片像素,只改变存储大小
Bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
2. 采样率压缩
graph LR
    A[原图: 1000x1000] --> B[采样率2]
    B --> C[结果: 500x500]
    D[内存减少4倍] --> C
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 采样率为2,宽高各缩小2倍,内存占用缩小4倍

四、图片缓存原理

graph TD
    A[三级缓存] --> B[内存缓存]
    A --> C[磁盘缓存]
    A --> D[网络获取]
    
    B --> E[LruCache]
    C --> F[DiskLruCache]
1. 内存缓存实现
public class ImageCache {
    private LruCache<String, Bitmap> mMemoryCache;
    
    public ImageCache() {
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8; // 使用1/8最大内存作为缓存
        
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount() / 1024;
            }
        };
    }
}

五、大图加载原理

graph TD
    A[大图加载] --> B[获取图片真实尺寸]
    B --> C[计算采样率]
    C --> D[按采样率加载]
    D --> E[内存使用]
1. BitmapRegionDecoder
// 分块加载大图片
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(path, false);
Rect rect = new Rect(0, 0, width, height); // 指定加载区域
Bitmap bitmap = decoder.decodeRegion(rect, options);

六、常见优化方案总结

  1. 加载优化

    • 根据控件大小计算采样率
    • 选择合适的解码格式
    • 异步加载
  2. 存储优化

    • 使用WebP格式
    • 图片压缩
    • 尺寸适配
  3. 显示优化

    • 图片预加载
    • 列表图片复用
    • 及时回收

七、完整加载流程

graph TD
    A[请求加载图片] --> B{检查内存缓存}
    B -->|有| C[直接显示]
    B -->|无| D{检查磁盘缓存}
    D -->|有| E[加载到内存]
    D -->|无| F[网络下载]
    F --> G[压缩处理]
    G --> H[存入缓存]
    H --> I[显示图片]

八、实战示例

public class ImageLoader {
    // 计算采样率
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int height = options.outHeight;
        int width = options.outWidth;
        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;
    }
    
    // 加载图片
    public static Bitmap decodeSampledBitmap(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);
    }
}

九、注意事项

  1. 在UI线程中避免直接加载大图
  2. 注意内存泄漏问题
  3. 合理设置缓存大小
  4. 及时回收不需要的Bitmap
  5. 根据实际需求选择合适的压缩方案

这就是图片优化的主要原理和实现方式。通过理解这些原理,我们可以在实际开发中更好地处理图片相关的性能问题。

问题5. APK瘦身

  1. 主要方法
    • 资源文件优化
    • 代码混淆
    • 移除无用资源
    • 动态下发资源
    • 图片压缩

详细解释

让我详细解释 APK 瘦身的底层原理:

APK 瘦身原理图解

graph TD
    A[APK瘦身] --> B[资源优化]
    A --> C[代码优化]
    A --> D[SO库优化]
    A --> E[打包优化]

    B --> B1[资源文件压缩]
    B --> B2[移除冗余资源]
    B --> B3[图片优化]
    B --> B4[资源混淆]

    C --> C1[代码混淆]
    C --> C2[移除冗余代码]
    C --> C3[动态下发]

    D --> D1[移除冗余架构]
    D --> D2[动态加载]

    E --> E1[签名方案优化]
    E --> E2[压缩算法优化]

一、资源优化原理

  1. 资源文件压缩
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" /> 
  1. 图片优化原理
  • WebP 转换:比 PNG 减少 25%~35% 体积
  • 矢量图形:使用 SVG 替代多个分辨率的 PNG
  • 图片压缩:
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
        }
    }
}

二、代码优化原理

  1. 代码混淆过程
graph LR
    A[源代码] --> B[混淆]
    B --> C[压缩]
    C --> D[优化]
    D --> E[混淆后代码]
  1. 冗余代码移除原理
  • 使用 ProGuard 分析代码调用链
  • 移除未使用的类、方法、字段
  • 内联简单方法
  • 简化类和方法名称

三、SO库优化原理

  1. 架构选择
android {
    defaultConfig {
        ndk {
            // 只保留主流架构
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}
  1. 动态加载原理
public class SOLoader {
    static {
        try {
            System.loadLibrary("your_lib");
        } catch (UnsatisfiedLinkError e) {
            // 动态下载并加载
        }
    }
}

四、打包优化原理

  1. 签名方案优化
  • V2/V3 签名比 V1 签名效率更高
  • 文件对齐优化:
zipalign -v 4 infile.apk outfile.apk
  1. 压缩算法优化
android {
    aaptOptions {
        cruncherEnabled = false // 关闭PNG压缩
    }
}

五、具体实施方案

  1. 资源优化实施
  • 使用 Android Studio 的 APK Analyzer 分析资源占用
  • 使用 Resource Shrinker 移除未使用资源
  • 使用 TinyPNG 等工具压缩图片
  • 使用 AndResGuard 进行资源混淆
  1. 代码优化实施
graph TD
    A[代码优化] --> B[使用ProGuard]
    A --> C[使用R8]
    B --> D[配置混淆规则]
    C --> E[自动优化]
    D --> F[测试验证]
    E --> F
  1. 动态化方案
  • 将非必需资源放到云端
  • 按需下载功能模块
  • 使用 App Bundle 实现动态分发

优化效果衡量

  1. 大小对比
graph LR
    A[原始APK] --> B[资源优化]
    B --> C[代码优化]
    C --> D[SO优化]
    D --> E[最终APK]
    
    style E fill:#9f9,stroke:#333,stroke-width:2px
  1. 监控指标
  • APK 大小变化
  • 下载转化率
  • 安装成功率
  • 启动时间
  • 运行内存

注意事项

  1. 保证稳定性
  • 混淆后要充分测试
  • 保留必要的类和方法
  • 保存映射文件用于追踪问题
  1. 动态化权衡
  • 考虑网络环境
  • 评估用户体验
  • 权衡首次下载大小和后续下载量

通过以上优化,一般可以减少 30%-50% 的 APK 体积。记住要根据具体项目情况选择合适的优化方案,并在优化过程中持续监控和测试,确保应用的稳定性不受影响。

问题6. 启动优化

graph LR
    A[冷启动优化] --> B[闪屏优化]
    B --> C[延迟初始化]
    C --> D[异步初始化]
    D --> E[减少布局层级]

详细解释

让我详细讲解 Android 应用启动优化的底层原理:

一、启动类型

graph TD
    A[启动类型] --> B[冷启动]
    A --> C[热启动]
    A --> D[温启动]
    B --> B1[进程未创建<br>耗时最长]
    C --> C1[进程在后台<br>耗时最短]
    D --> D1[进程存在<br>Activity被销毁]

二、冷启动流程

graph TB
    A[点击应用图标] --> B[Zygote进程fork]
    B --> C[创建应用进程]
    C --> D[创建Application对象]
    D --> E[启动主线程]
    E --> F[创建MainActivity]
    F --> G[加载布局绘制]
    G --> H[显示首帧]

三、启动耗时原因

  1. 系统层面

    • Zygote进程fork耗时
    • 应用进程创建耗时
    • ART/Dalvik虚拟机启动
  2. 应用层面

graph LR
    A[应用层耗时] --> B[Application初始化]
    A --> C[首页布局复杂]
    A --> D[第三方SDK初始化]
    A --> E[IO操作]
    A --> F[复杂计算]

四、优化方案详解

1. Application优化
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 1. 异步初始化
        AsyncInitializer.start(this);
        
        // 2. 延迟初始化
        delayInit();
    }
    
    private void delayInit() {
        new Handler().postDelayed(() -> {
            // 延迟初始化第三方SDK
            initThirdPartySDK();
        }, 3000);
    }
}
2. 闪屏优化
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- 设置闪屏背景,避免白屏 -->
    <item name="android:windowBackground">@drawable/splash_bg</item>
</style>

五、具体优化技术

  1. 类预加载优化
graph LR
    A[预加载优化] --> B[提前加载类]
    B --> C[减少首次使用时间]
    A --> D[类校验优化]
    D --> E[减少启动时验证]
  1. 布局优化
<!-- 使用ViewStub延迟加载不常用布局 -->
<ViewStub
    android:id="@+id/stub_import"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/progress_overlay" />
  1. 线程优化
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 使用线程池管理初始化任务
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 并行执行初始化任务
        CompletableFuture.runAsync(() -> {
            initData();
        }, executor);
        
        CompletableFuture.runAsync(() -> {
            initNetwork();
        }, executor);
    }
}

六、启动优化监控

graph TD
    A[启动监控] --> B[函数插桩]
    A --> C[TraceView]
    A --> D[Systrace]
    B --> E[统计各阶段耗时]
    C --> F[分析方法耗时]
    D --> G[分析系统调用]

七、优化效果衡量

  1. 关键指标

    • 首屏时间
    • 可交互时间
    • 完全加载时间
  2. 测量方法

public class TimeRecorder {
    private static long sStartTime;
    
    public static void start() {
        sStartTime = System.currentTimeMillis();
    }
    
    public static void record(String tag) {
        long cost = System.currentTimeMillis() - sStartTime;
        Log.d("启动耗时", tag + ": " + cost + "ms");
    }
}

八、最佳实践总结

  1. 启动前优化

    • 闪屏优化
    • 提前加载
    • 类验证优化
  2. 启动中优化

    • 异步初始化
    • 延迟初始化
    • 懒加载
    • 并行初始化
  3. 启动后优化

    • 后台任务延迟
    • 预加载优化
    • 持续监控

注意事项:

  1. 区分首次启动和非首次启动场景
  2. 注意异步初始化的依赖关系
  3. 建立完整的启动优化监控体系
  4. 持续优化和迭代

通过以上优化方案,可以显著提升应用的启动速度,提供更好的用户体验。记住,启动优化是一个持续的过程,需要不断监控和改进。

问题7. WebView优化

  1. 优化方案
    • 预加载WebView
    • 离线缓存
    • 延迟加载
    • 资源拦截
    • DNS优化

详细解释

让我详细讲解WebView优化的底层原理:

WebView优化的底层原理

graph TD
    A[WebView优化] --> B[预加载机制]
    A --> C[资源加载优化]
    A --> D[缓存机制]
    A --> E[内存优化]
    A --> F[通信优化]
1. 预加载机制原理
public class WebViewManager {
    private static WebView mWebView;
    
    public static void preload(Context context) {
        // 在应用启动时就初始化WebView
        mWebView = new WebView(context.getApplicationContext());
        // 预先加载常用配置
        WebSettings settings = mWebView.getSettings();
        settings.setJavaScriptEnabled(true);
        // ... 其他配置
    }
}

工作原理

  1. 应用冷启动时就创建WebView实例
  2. WebView首次初始化需要约100-200ms
  3. 预加载可以将这部分时间提前,提升打开速度
2. 资源加载优化
graph LR
    A[资源请求] --> B[拦截器判断]
    B --> C[本地资源]
    B --> D[网络请求]
    C --> E[直接加载]
    D --> F[缓存策略]

实现原理

public class CustomWebViewClient extends WebViewClient {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        String url = request.getUrl().toString();
        // 1. 检查本地是否有离线资源包
        if (OfflineResourceManager.hasOfflineResource(url)) {
            return OfflineResourceManager.getOfflineResource(url);
        }
        // 2. 检查是否命中缓存
        if (WebResourceCache.hasCache(url)) {
            return WebResourceCache.getCache(url);
        }
        // 3. 网络请求
        return super.shouldInterceptRequest(view, request);
    }
}
3. 缓存机制原理
  1. 多级缓存
graph TD
    A[资源请求] --> B[内存缓存]
    B -->|未命中| C[磁盘缓存]
    C -->|未命中| D[网络请求]
    D --> E[存入缓存]
  1. 实现方式
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
webSettings.setDomStorageEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setAppCachePath(context.getCacheDir().getAbsolutePath());
4. 内存优化原理
  1. WebView池化管理
public class WebViewPool {
    private Queue<WebView> webViewPool;
    
    public WebView obtain() {
        if (webViewPool.isEmpty()) {
            return createWebView();
        }
        return webViewPool.poll();
    }
    
    public void recycle(WebView webView) {
        webView.loadUrl("about:blank");
        webView.clearHistory();
        webViewPool.offer(webView);
    }
}
  1. 内存回收原理
graph TD
    A[WebView销毁] --> B[清空内容]
    B --> C[清除缓存]
    C --> D[移除回调]
    D --> E[放入对象池]
5. 通信优化原理
  1. JS桥接优化
public class JSBridge {
    // 原生方法映射表
    private static Map<String, Method> nativeMethodMap = new HashMap<>();
    
    @JavascriptInterface
    public void invoke(String methodName, String params) {
        // 通过反射直接调用原生方法,避免多次解析
        Method method = nativeMethodMap.get(methodName);
        if (method != null) {
            method.invoke(params);
        }
    }
}
  1. 通信流程
graph LR
    A[JS调用] --> B[JSBridge]
    B --> C[方法映射]
    C --> D[原生方法]
    D --> E[结果回调]
6. 白屏优化原理
  1. 错误页面处理
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
    // 加载本地错误页
    view.loadUrl("file:///android_asset/error.html");
    // 错误上报
    ErrorReporter.report(errorCode, description);
}
  1. 进度反馈
@Override
public void onProgressChanged(WebView view, int newProgress) {
    if (newProgress == 100) {
        progressBar.setVisibility(View.GONE);
    } else {
        progressBar.setVisibility(View.VISIBLE);
        progressBar.setProgress(newProgress);
    }
}
优化效果
  1. 首屏加载速度提升50%-70%
  2. 内存占用减少30%-50%
  3. 页面白屏率降低80%以上
  4. 离线访问成功率提升95%
注意事项
  1. WebView预加载要在主进程进行
  2. 资源拦截要注意性能消耗
  3. 缓存策略要根据业务场景选择
  4. 内存回收要及时
  5. JS通信要考虑安全性

通过以上优化,可以显著提升WebView的性能和用户体验。关键是要根据实际业务场景,选择合适的优化策略组合使用。

Webview跨域问题

我来用通俗易懂的方式解释 Android WebView 跨域问题的底层原理。

🌟 什么是跨域?

想象一下,每个网站都是一个独立的小区:

  • https://a.com 是 A 小区
  • https://b.com 是 B 小区

跨域就像是 A 小区的居民想要去 B 小区借东西,但被门卫拦住了,这就是浏览器的"同源策略"。

🔒 同源策略的三要素

小区地址必须完全一致:

  1. 协议(Protocol):是坐公交还是开车去(http/https)
  2. 域名(Host):具体哪个小区(domain)
  3. 端口(Port):从哪个门进去(80/443等)
📱 Android WebView 跨域原理
1. 默认情况
WebView webView = new WebView(context);
// 默认情况下,WebView 严格执行同源策略
// A 网站的 JavaScript 不能访问 B 网站的内容
2. 开启跨域访问
WebSettings settings = webView.getSettings();
// 允许 JavaScript 执行
settings.setJavaScriptEnabled(true);
// 允许跨域访问
settings.setAllowUniversalAccessFromFileURLs(true);
settings.setAllowFileAccessFromFileURLs(true);
🔄 跨域解决方案流程图
graph TD
    A[网页发起跨域请求] --> B{检查 WebView 设置}
    B -->|默认设置| C[被同源策略拦截]
    B -->|开启跨域设置| D[允许访问]
    D --> E[数据交互]
    C --> F[请求失败]
💡 常见解决方案
  1. 服务器端设置 CORS
location / {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
}
  1. WebView 端设置
webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onLoadResource(WebView view, String url) {
        // 允许加载任何资源
        super.onLoadResource(view, url);
    }
});
🚀 实际应用场景
  1. 混合开发(Hybrid App)
  • Web 页面需要调用原生功能
  • 原生 App 需要向 Web 页面注入数据
  1. 本地 HTML 加载
  • 加载本地 HTML 文件时访问网络资源
  • 本地 HTML 之间的互相访问
⚠️ 安全注意事项
  1. 不要过度开放跨域权限
  2. 仅对可信域名开放必要的权限
  3. 注意防范 XSS 和 CSRF 攻击
🎯 总结

Android WebView 的跨域机制就像一个安保系统:

  • 默认情况下,所有跨域访问都会被拦截
  • 通过适当的设置,可以为可信的访问开启绿色通道
  • 但要注意平衡安全性和便利性

记住:跨域限制是为了保护用户数据安全,在解决跨域问题时要谨慎,不要过度放开权限。

WebView 处理Session和Cookie

让我详细解释 Android 应用中如何将登录 session 或 cookie 传递给 WebView。

🌟 主要实现方式
1. 通过 CookieManager 管理 Cookie
// 获取 CookieManager 实例
CookieManager cookieManager = CookieManager.getInstance();
// 启用 cookie
cookieManager.setAcceptCookie(true);

// 设置 cookie
public void syncCookie(String url, String cookie) {
    // ... 前面的代码 ...
    
    // 设置 cookie
    cookieManager.setCookie(url, cookie);
    
    // 同步 cookie
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cookieManager.flush();
    } else {
        CookieSyncManager.getInstance().sync();
    }
}
2. 在 WebView 加载页面时注入 Cookie
webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        
        // 在页面加载时注入 cookie
        String cookie = "sessionId=abc123; token=xyz789";
        CookieManager.getInstance().setCookie(url, cookie);
    }
});
🔄 完整的 Cookie 同步流程
graph TD
    A[原生 App 登录] --> B[获取 Session/Cookie]
    B --> C[CookieManager 设置 Cookie]
    C --> D[WebView 加载页面]
    D --> E[自动携带 Cookie]
    E --> F[服务器验证身份]
📱 实际应用示例
1. 登录后同步 Cookie
public void onLoginSuccess(String sessionId, String token) {
    // 构建 cookie 字符串
    String cookie = String.format("sessionId=%s; token=%s", sessionId, token);
    String domain = "example.com";
    
    // 设置 cookie
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    cookieManager.setCookie(domain, cookie);
    
    // 强制同步
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cookieManager.flush();
    }
}
2. 自定义 WebViewClient 处理请求头
public class CustomWebViewClient extends WebViewClient {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        // 获取原始请求
        String url = request.getUrl().toString();
        
        // 创建新的请求,添加自定义 header
        try {
            URL urlObj = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection();
            
            // 添加认证信息
            conn.addRequestProperty("Cookie", CookieManager.getInstance().getCookie(url));
            // ... 处理响应 ...
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return super.shouldInterceptRequest(view, request);
    }
}
🔍 检查 Cookie 是否同步成功
private void checkCookies(String url) {
    CookieManager cookieManager = CookieManager.getInstance();
    String cookies = cookieManager.getCookie(url);
    Log.d("Cookie", "Current cookies: " + cookies);
}
⚠️ 注意事项
  1. 安全考虑
  • 确保使用 HTTPS 传输
  • 设置 cookie 的 secure 标志
  • 合理设置 cookie 过期时间
  1. 兼容性处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true);
}
  1. 清理 Cookie
public void clearCookies() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        CookieManager.getInstance().removeAllCookies(null);
        CookieManager.getInstance().flush();
    } else {
        CookieManager.getInstance().removeAllCookie();
        CookieSyncManager.getInstance().sync();
    }
}
💡 最佳实践
  1. 统一管理 Cookie
  • 创建 CookieManager 工具类
  • 封装常用的 Cookie 操作
  1. 同步时机
  • 登录成功后立即同步
  • WebView 加载前确保同步完成
  • 定期检查 Cookie 有效性
  1. 错误处理
  • 监控 Cookie 设置是否成功
  • 处理 Cookie 过期的情况
  • 提供重新登录的机制

通过以上方式,可以确保 Android 原生应用和 WebView 之间的身份认证信息顺利传递,实现无缝的用户体验。