从某程序员开发外卖配送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>>
