从某程序员开发外卖配送APP到性能优化全记录Android编程实例分析

说实话,写这段代码的时候我还只是个刚入行的Android开发者,现在回过头看,那段日子简直像在做一场马拉松——一开始以为跑百米,跑着跑着发现是五十公里越野。

事情是这样的。2023年初,我和两个朋友打算做一款专注校园外卖的配送APP。为什么是校园?因为我们自己就是大学生,太懂那种下雨天不想出门、又不想吃食堂的绝望感了。我们叫它”饿虎”,名字糙了点,但挺好记。


第一阶段:把功能跑通,先把东西做出来

刚开始那几个月,我们三个人的目标是尽快把APP做出来上线。代码写得那叫一个”快”。

定位功能是这样写的:

// 这个代码我至今还记得, shame on me
public class LocationService {
    
    private LocationManager locationManager;
    private Location currentLocation;
    
    public Location getLocation() {
        // 直接返回上次定位,没有考虑时间戳
        if (currentLocation != null) {
            return currentLocation;
        }
        
        // 每次调用都重新申请权限
        String permission = Manifest.permission.ACCESS_FINE_LOCATION;
        // 没有缓存,没有节流
        
        Criteria criteria = new Criteria();
        criteria.setAccuracy(Criteria.ACCURACY_FINE);
        criteria.setCostAllowed(true);
        
        String provider = locationManager.getBestProvider(criteria, true);
        
        // 直接获取位置,没有任何异步处理
        currentLocation = locationManager.getLastKnownLocation(provider);
        
        return currentLocation;
    }
    
    // 更糟的是,每次下单都重新创建LocationManager
    public LocationService() {
        locationManager = (LocationManager) 
            MyApplication.getContext()
                .getSystemService(Context.LOCATION_SERVICE);
    }
}

这段代码有什么问题?我慢慢才知道。但那时候我觉得能跑就行。

订单列表页更是灾难:

// 主线程直接查数据库 + 网络请求,没有异步
// 每次刷新都重新请求
public class OrderListActivity extends AppCompatActivity {
    
    private RecyclerView recyclerView;
    private OrderAdapter adapter;
    private List<Order> orders = new ArrayList<>();
    
    // 问题1:没有分页,一次加载全部订单
    // 问题2:主线程做网络请求
    // 问题3:图片加载用 Picasso 但没做任何缓存策略
    
    public void loadOrders() {
        // 这是主线程!绝对不能在 UI 线程做网络请求
        new Thread(() -> {
            // 问题4:没有取消机制,多次点击会创建多个线程
            List<Order> orders = orderRepository.getAllOrders();
            
            runOnUiThread(() -> {
                // 问题5:每次都创建新的 Adapter
                adapter = new OrderAdapter(orders);
                recyclerView.setAdapter(adapter);
                
                // 问题6:没有 DiffUtil,每次都全量刷新
                adapter.notifyDataSetChanged();
            });
        }).start();
    }
}

那时候我们的APP在模拟器上跑得还行,但一到真机上——特别是低端机——就卡得让人怀疑人生。

第一次内测的时候,我发到学校群里让大家下载试试。半小时后,微信响个不停。

“这APP是PPT做的吗?” “为什么我点了三次就崩了?” “图片怎么加载得这么慢?” “定位怎么老是定位不准?”

那一刻我真的脸红到耳根。但骂归骂,问题总得解决。


第二阶段:发现问题,把bug一个一个揪出来

2.1 用Profiler发现性能杀手

Android Studio自带的Profiler是个好东西,之前我从来不仔细用,觉得它花里胡哨。这次我老老实实打开,开始记录性能数据。

内存问题触目惊心:

分配内存:450MB
 retained 内存:280MB
 GC次数:每秒15-20次

这是什么概念?一台普通安卓手机总内存也就512MB到1GB。一个APP就占了将近一半,GC(垃圾回收)频繁到每秒钟十几次,这手机能不发烫才怪。

CPU使用率:

主要CPU消耗:
- 主线程:45%(主要是UI绘制和动画)
- 图片解码线程:30%
- 网络请求线程:15%
- 其他:10%

2.2 用LeakCanary抓内存泄漏

LeakCanary是我见过最有用的工具之一。打开它之后,第一个泄漏就给我震住了:

// 这个泄漏藏了整整两个月
// 静态变量持有Activity引用
public class OrderDetailsFragment {
    
    // 错误做法:静态持有Context
    private static Context staticContext;
    
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        staticContext = context; // 这里泄漏了Activity!
    }
    
    // 更糟的是,还有View泄漏
    private TextView orderStatus;
    
    @Override
    public View onCreateView(...) {
        View root = LayoutInflater.from(context).inflate(R.layout.fragment_order, container, false);
        orderStatus = root.findViewById(R.id.tv_order_status);
        
        // 问题:把View存到了静态Map里
        OrderCache.INSTANCE.getViews().put(orderId, orderStatus);
        
        return root;
    }
}

还有一个更隐蔽的泄漏:

// 单例模式用错了,导致Context泄漏
public class OrderRepository {
    
    // 问题:把Application Context传递下去了
    // 但某些地方误传了Activity Context
    public static OrderRepository getInstance(Context context) {
        if (instance == null) {
            instance = new OrderRepository(context); // context可能是Activity!
        }
        return instance;
    }
    
    private OrderRepository(Context context) {
        this.context = context; // 这里悄悄泄漏了整个Activity
    }
}

第三阶段:针对性优化,每一行代码都有意义

3.1 内存优化:从垃圾堆里抢救生命

定位服务的改造:

// 优化后的定位服务
public class LocationService {
    
    // 使用单例,避免重复创建
    private static volatile LocationService instance;
    
    private LocationManager locationManager;
    private Location currentLocation;
    private long lastUpdateTime;
    private static final long MIN_UPDATE_INTERVAL = 5000; // 5秒最小间隔
    private static final int MIN_ACCURACY = 100; // 100米最小精度
    
    // 使用Application Context,避免泄漏
    private LocationService(Context applicationContext) {
        locationManager = (LocationManager) 
            applicationContext.getSystemService(Context.LOCATION_SERVICE);
    }
    
    public static LocationService getInstance(Context context) {
        if (instance == null) {
            synchronized (LocationService.class) {
                if (instance == null) {
                    // 关键:传入Application Context,不是Activity Context
                    instance = new LocationService(
                        context.getApplicationContext()
                    );
                }
            }
        }
        return instance;
    }
    
    public Location getLocation() {
        // 节流:5秒内不重复定位
        long now = System.currentTimeMillis();
        if (currentLocation != null && 
            now - lastUpdateTime < MIN_UPDATE_INTERVAL) {
            return currentLocation;
        }
        
        // 精度过滤:只返回精度足够高的位置
        if (currentLocation != null && 
            currentLocation.getAccuracy() > MIN_ACCURACY) {
            currentLocation = null; // 精度不够,重新获取
        }
        
        // 使用RequestLocationUpdate + 线程池,避免主线程阻塞
        executor.execute(() -> {
            try {
                Criteria criteria = new Criteria();
                criteria.setAccuracy(Criteria.ACCURACY_FINE);
                criteria.setCostAllowed(true);
                criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
                
                String provider = locationManager.getBestProvider(
                    criteria, true
                );
                
                if (provider != null) {
                    currentLocation = locationManager.getLastKnownLocation(provider);
                    
                    // GPS定位补充
                    locationManager.requestLocationUpdates(
                        provider, 
                        MIN_UPDATE_INTERVAL, 
                        MIN_ACCURACY,
                        locationListener
                    );
                }
                
                lastUpdateTime = System.currentTimeMillis();
                
            } catch (SecurityException e) {
                Log.e(TAG, "定位权限异常", e);
            }
        });
        
        return currentLocation;
    }
    
    // 正确的权限申请方式
    public void requestLocationPermission(Activity activity) {
        if (ContextCompat.checkSelfPermission(
                activity, 
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED) {
            
            ActivityCompat.requestPermissions(
                activity,
                new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                REQUEST_LOCATION_PERMISSION
            );
        }
    }
    
    // 注意:在适当的时候释放定位监听
    public void release() {
        if (locationManager != null) {
            try {
                locationManager.removeUpdates(locationListener);
            } catch (Exception e) {
                Log.e(TAG, "释放定位监听失败", e);
            }
        }
    }
}

单例模式修正:

// 修正后的单例,使用Application Context
public class OrderRepository {
    
    private static volatile OrderRepository instance;
    private final Context applicationContext;
    
    // 私有构造函数,禁止传入Activity Context
    private OrderRepository(Application application) {
        this.applicationContext = application.getApplicationContext();
    }
    
    public static OrderRepository getInstance(Application application) {
        if (instance == null) {
            synchronized (OrderRepository.class) {
                if (instance == null) {
                    instance = new OrderRepository(application);
                }
            }
        }
        return instance;
    }
    
    // 获取ApplicationContext的工具方法
    public Context getContext() {
        return applicationContext;
    }
}

3.2 图片加载优化:从OOM到流畅

外卖APP里图片是最占内存的资源。餐厅头像、菜品照片、骑手图片——每一张都可能是个几MB的JPG。

原来是这样用的:

// 原始代码,没有任何优化
Picasso.get()
    .load(url)
    .into(imageView);

优化后的图片加载策略:

// 自定义图片加载器,针对外卖场景优化
public class FoodImageLoader {
    
    private static final int MAX_IMAGE_SIZE = 800; // 最大加载尺寸
    private static final int DISK_CACHE_SIZE_MB = 200; // 200MB磁盘缓存
    
    // 使用Glide代替Picasso,Glide对内存管理更智能
    private static Glide glide = GlideApp.get(MyApplication.getInstance());
    
    // 预定义多种尺寸的请求
    public static void loadAvatar(RoundImageView imageView, String url) {
        glide.load(url)
            .override(120, 120)           // 头像120x120
            .centerCrop()
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .placeholder(R.drawable.avatar_placeholder)
            .error(R.drawable.avatar_error)
            .into(imageView);
    }
    
    public static void loadFoodImage(ImageView imageView, String url) {
        glide.load(url)
            .override(400, 300)           // 菜品图400x300
            .centerCrop()
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .placeholder(R.drawable.food_placeholder)
            .error(R.drawable.food_error)
            .thumbnail(0.1f)              // 首帧缩略图
            .into(imageView);
    }
    
    public static void loadDeliveryPerson(ImageView imageView, String url) {
        glide.load(url)
            .override(200, 200)
            .circleCrop()
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .placeholder(R.drawable.person_placeholder)
            .error(R.drawable.person_error)
            .into(imageView);
    }
}

RecyclerView的图片优化:

// 自定义Adapter,使用DiffUtil
public class FoodAdapter extends RecyclerView.Adapter<FoodViewHolder> {
    
    private List<FoodItem> originalList;
    private List<FoodItem> currentList;
    
    @Override
    public void onBindViewHolder(FoodViewHolder holder, int position) {
        FoodItem item = currentList.get(position);
        
        // 只在绑定可见视图时才加载图片
        FoodImageLoader.loadFoodImage(
            holder.foodImage, 
            item.getImageUrl()
        );
        
        holder.foodName.setText(item.getName());
        holder.foodPrice.setText("¥" + item.getPrice());
    }
    
    // 使用DiffUtil精确更新,避免全量刷新
    public void updateData(List<FoodItem> newList) {
        FoodDiffCallback diffCallback = new FoodDiffCallback(
            originalList, newList
        );
        DiffUtil.DiffResult diffResult = 
            DiffUtil.calculateDiff(diffCallback);
        
        originalList = newList;
        currentList = newList;
        
        // 精确通知变化,而不是notifyDataSetChanged()
        diffResult.dispatchUpdatesTo(this);
    }
}

// DiffUtil的回调实现
public class FoodDiffCallback extends DiffUtil.ItemCallback<FoodItem> {
    
    @Override
    public boolean areItemsTheSame(FoodItem oldItem, FoodItem newItem) {
        return oldItem.getId() == newItem.getId();
    }
    
    @Override
    public boolean areContentsTheSame(FoodItem oldItem, FoodItem newItem) {
        return oldItem.equals(newItem);
    }
}

3.3 主线程优化:把该放的都放出去

外卖APP有个核心场景:用户下单后,订单状态实时更新。原来我们用的是轮询,每秒请求一次接口,这简直就是自杀。

原来主线程被占满的罪魁祸首:

// 定时轮询,每秒请求一次 - 这是绝对错误的做法
private void startPolling() {
    new CountDownTimer(60000, 1000) {
        public void onTick(long millisUntilFinished) {
            // 每秒都在主线程做网络请求
            checkOrderStatus(); // 问题:这是网络请求!
        }
        public void onFinish() {}
    }.start();
}

private void checkOrderStatus() {
    // 问题1:在主线程做网络请求
    // 问题2:每秒一次,服务器压力巨大
    // 问题3:用户没下单的时候也在轮询
    
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder()
        .url("https://api.example.com/order/status?orderId=" + orderId)
        .build();
    
    client.newCall(request).enqueue(callback); // 虽然是enqueue,
                                               // 但每秒都在创建请求
}

优化后的WebSocket长连接:

// 使用WebSocket实现订单状态实时推送
public class OrderWebSocketManager {
    
    private static final String ORDER_WEBSOCKET_URL = 
        "wss://api.example.com/ws/order";
    
    private WebSocket webSocket;
    private OkHttpClient client;
    private OrderWebSocketListener listener;
    
    public void connect(OrderWebSocketListener listener) {
        this.listener = listener;
        this.client = new OkHttpClient.Builder()
            .readTimeout(30, TimeUnit.SECONDS)
            .build();
        
        Request request = new Request.Builder()
            .url(ORDER_WEBSOCKET_URL)
            .build();
        
        // 连接时传入userId,服务端会根据用户推送相关订单
        this.webSocket = client.newWebSocket(request, 
            new WebSocketListener() {
                @Override
                public void onOpen(WebSocket ws, Response response) {
                    listener.onConnected();
                }
                
                @Override
                public void onMessage(WebSocket ws, String text) {
                    // 收到订单状态更新
                    OrderStatusUpdate update = 
                        parseOrderUpdate(text);
                    
                    if (update != null) {
                        // 在主线程回调,更新UI
                        runOnUiThread(() -> 
                            listener.onOrderUpdate(update)
                        );
                    }
                }
                
                @Override
                public void onFailure(WebSocket ws, Throwable t, Response response) {
                    // 断线重连
                    scheduleReconnect();
                }
            }
        );
    }
    
    private void scheduleReconnect() {
        // 指数退避重连策略
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            connect(listener);
        }, 3000); // 3秒后重连
    }
    
    public void disconnect() {
        if (webSocket != null) {
            webSocket.close(1000, "Normal closure");
        }
    }
}

推送服务的接入:

// 接入FCM推送,替代轮询
public class OrderPushManager {
    
    private static final String ORDER_TOPIC = "order_status";
    
    public void subscribeOrderUpdates(String userId) {
        // 订阅订单主题
        FirebaseMessaging.getInstance()
            .subscribeToTopic(ORDER_TOPIC);
        
        // 注册消息监听
        FirebaseMessaging.getInstance().subscribeToTopic(
            "user_" + userId + "_orders"
        );
    }
    
    public void unsubscribeOrderUpdates() {
        FirebaseMessaging.getInstance()
            .unsubscribeFromTopic(ORDER_TOPIC);
    }
    
    // 处理推送消息
    @Override
    public void onMessageReceived(RemoteMessage message) {
        if (message.getData().containsKey("order_id")) {
            String orderId = message.getData().get("order_id");
            String status = message.getData().get("status");
            
            // 更新本地数据库
            orderRepository.updateOrderStatus(orderId, status);
            
            // 通知UI更新
            OrderStatusManager.getInstance()
                .notifyOrderUpdate(orderId, status);
        }
    }
}

3.4 数据库优化:从关系型到查询优化

外卖APP的订单表、商品表、用户表数据量越来越大。原来的查询方式简直是灾难。

原始慢查询:

// 每次获取订单都全表扫描
public List<Order> getAllOrders() {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    
    // 问题:没有索引,全表扫描
    // 问题:没有分页,一次加载全部
    // 问题:没有在子线程执行
    Cursor cursor = db.query(
        "orders",
        null, // 全部列
        null, // 没有WHERE条件
        null,
        null,
        null,
        "create_time DESC" // 没有LIMIT
    );
    
    List<Order> orders = new ArrayList<>();
    while (cursor.moveToNext()) {
        orders.add(parseOrder(cursor));
    }
    cursor.close();
    
    return orders; // 可能返回几千条记录!
}

优化后的数据库操作:

// 使用Room + LiveData,加上分页
public class OrderDao {
    
    // 添加索引,加速查询
    @Query("SELECT * FROM orders WHERE user_id = :userId " +
           "ORDER BY create_time DESC")
    DataSource.Factory<Integer, Order> loadByUserIdWithFactory(int userId);
    
    // 只查询需要的列,减少内存占用
    @Query("SELECT id, status, total_price, create_time " +
           "FROM orders WHERE user_id = :userId " +
           "ORDER BY create_time DESC LIMIT :limit OFFSET :offset")
    List<Order> loadPagedOrders(
        int userId, 
        int limit, 
        int offset
    );
    
    // 使用索引字段查询
    @Query("SELECT * FROM orders WHERE order_id = :orderId LIMIT 1")
    Order getById(String orderId);
    
    // 批量更新状态
    @Query("UPDATE orders SET status = :newStatus " +
           "WHERE order_id IN (:orderIds)")
    abstract void batchUpdateStatus(List<String> orderIds, int newStatus);
    
    // 定时清理历史数据
    @Query("DELETE FROM orders WHERE create_time < :cutoffTime")
    abstract int deleteOldOrders(long cutoffTime);
}

// 使用Paging 3.0实现分页加载
public class OrderPagingSource : PagingSource<Int, Order>() {
    
    private val repository = OrderRepository.getInstance(app)
    
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Order> {
        return try {
            val page = params.key ?: 1
            val pageSize = params.loadSize
            
            val result = repository.loadPagedOrders(
                userId = currentUser.id,
                limit = pageSize,
                offset = (page - 1) * pageSize
            )
            
            LoadResult.Page(
                data = result,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (result.isEmpty()) null else page + 1
            )
            
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}

数据库初始化和索引创建:

-- 创建订单表时直接建立索引
CREATE TABLE orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id TEXT NOT NULL UNIQUE,
    user_id TEXT NOT NULL,
    restaurant_id TEXT NOT NULL,
    status INTEGER NOT NULL,
    total_price REAL NOT NULL,
    create_time INTEGER NOT NULL,
    update_time INTEGER NOT NULL
);

-- 创建索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_create_time ON orders(create_time);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- 创建视图用于常用查询
CREATE VIEW order_summary AS
SELECT 
    user_id,
    COUNT(*) as total_orders,
    SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as completed_orders,
    SUM(total_price) as total_spent
FROM orders
GROUP BY user_id;

3.5 网络层优化:从盲目请求到智能缓存

外卖APP的网络请求非常多:加载餐厅列表、查询商品、下单、支付、查看订单状态。原来我们每个接口都是裸请求,没有任何优化。

网络层重构:

// 统一的网络管理器
public class OrderNetworkManager {
    
    private final OkHttpClient httpClient;
    private final Gson gson;
    private final Cache cache;
    
    public OrderNetworkManager(Application application) {
        // 配置HTTP缓存
        int cacheSize = 10 * 1024 * 1024; // 10MB缓存
        File cacheDir = new File(application.getCacheDir(), "http_cache");
        this.cache = new Cache(cacheDir, cacheSize);
        
        this.httpClient = new OkHttpClient.Builder()
            .cache(cache)
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(15, TimeUnit.SECONDS)
            .addInterceptor(new CacheInterceptor())
            .addInterceptor(new LoggingInterceptor())
            .build();
        
        this.gson = new GsonBuilder()
            .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
            .create();
    }
}

// 缓存拦截器
public class CacheInterceptor implements Interceptor {
    
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        
        // 列表类请求:缓存5分钟
        if (request.url().toString().contains("/api/restaurants")) {
            request = request.newBuilder()
                .header("Cache-Control", "public, max-age=300")
                .build();
        }
        
        // 商品类请求:缓存10分钟
        if (request.url().toString().contains("/api/foods")) {
            request = request.newBuilder()
                .header("Cache-Control", "public, max-age=600")
                .build();
        }
        
        Response response = chain.proceed(request);
        
        // 写回缓存
        if (response.cacheResponse() == null) {
            response = response.newBuilder()
                .header("Cache-Control", "public, max-age=300")
                .build();
        }
        
        return response;
    }
}

API接口的封装:

”`java // 使用Retrofit封装API接口 public interface OrderApiService {

// 餐厅列表,带分页和缓存
@GET("api/restaurants")
Call<ApiResponse<List<Restaurant>>> getRestaurants(
    @Query("page") int page,
    @Query("limit") int limit,
    @Query("location_lat") double lat,
    @Query("location_lng") double lng
);

// 餐厅详情
@GET("api/restaurants/{id}")
Call<ApiResponse<RestaurantDetail>> getRestaurantDetail(
    @Path("id") String restaurantId
);

// 菜品列表,带分类
@GET("api/restaurants/{id}/foods")
Call<ApiResponse<List<F>>