67、重构与移动端代码:重构Android/iOS代码中的坏味道
移动端开发,说白了就是跟屏幕尺寸、电池寿命、网络延迟这三座大山打交道。我做了这么多年Android和iOS的重构,发现很多坏味道其实跨平台通用。今天咱们就聊聊那些在移动端代码里最常见的“臭味”,以及怎么把它们清理干净。
一、移动端特有的坏味道清单
先列个清单,这些都是我在项目中反复遇到的。你想想看,是不是也眼熟?
| 坏味道类型 | Android表现 | iOS表现 | 危害等级 |
|---|---|---|---|
| 巨型Activity/ViewController | Activity上千行,逻辑全塞在onCreate里 | ViewController的viewDidLoad写了200行 | ⭐⭐⭐⭐⭐ |
| 硬编码字符串/尺寸 | 直接写"#FF0000"、320dp | 直接写CGRectMake(0,0,320,480) | ⭐⭐⭐ |
| 网络请求散落各处 | 每个Activity里都new一个OkHttp | 每个VC里都写URLSession | ⭐⭐⭐⭐ |
| UI更新与业务逻辑耦合 | Adapter里写网络回调 | Cell里直接调API | ⭐⭐⭐⭐⭐ |
| 线程管理混乱 | 到处new Thread() | 主线程做耗时操作 | ⭐⭐⭐⭐ |
二、巨型Activity/ViewController——拆!
我个人习惯,一个Activity超过300行就该警惕了。超过500行,基本就是重构信号。为什么会这样?因为移动端的UI生命周期和业务逻辑太容易纠缠在一起了。
举个例子,我曾经接手过一个Android项目,一个Activity有1800行。里面混着网络请求、数据库操作、UI更新、动画控制……简直就是个垃圾场。
重构思路很简单:按职责拆分。
核心原则:Activity/ViewController只负责生命周期和UI事件分发,业务逻辑交给Presenter/ViewModel,数据操作交给Repository。
看个Android的例子:
// 重构前:Activity里什么都有
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 网络请求直接写在这里
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 直接更新UI
runOnUiThread(() -> textView.setText("加载失败"));
}
@Override
public void onResponse(Call call, Response response) {
// 解析数据、更新UI全在这里
}
});
}
}
// 重构后:职责清晰
public class MainActivity extends AppCompatActivity {
private MainViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
viewModel.getData().observe(this, data -> {
// 只做UI更新
textView.setText(data.toString());
});
findViewById(R.id.button).setOnClickListener(v -> {
viewModel.loadData();
});
}
}
// ViewModel处理业务逻辑
public class MainViewModel extends ViewModel {
private MutableLiveData<String> data = new MutableLiveData<>();
private DataRepository repository = new DataRepository();
public LiveData<String> getData() { return data; }
public void loadData() {
repository.fetchData(new Callback<String>() {
@Override
public void onSuccess(String result) {
data.postValue(result);
}
@Override
public void onError(String error) {
data.postValue("加载失败: " + error);
}
});
}
}
我的经验:iOS的ViewController重构思路完全一样。把网络请求扔到Service层,数据解析扔到Model层,VC只负责把数据填到UI上。我曾经把一个800行的VC拆成了5个文件,每个文件不到200行,维护起来舒服多了。
三、硬编码的噩梦
移动端开发最怕什么?屏幕适配。硬编码尺寸和颜色简直就是给自己挖坑。
Android里我见过有人直接写textView.setTextSize(16),iOS里写label.font = [UIFont systemFontOfSize:16]。这样做的问题在于:换一套设计规范,你得改几百个地方。
正确的做法:
- Android:用dimens.xml和colors.xml统一管理
- iOS:用常量文件或Asset Catalog管理
- 跨平台:用设计Token系统
// Android:dimens.xml
<resources>
<dimen name="text_size_body">16sp</dimen>
<dimen name="margin_standard">16dp</dimen>
<color name="primary">#6200EE</color>
</resources>
// 使用
textView.setTextSize(getResources().getDimension(R.dimen.text_size_body));
// iOS:Constants.h
static CGFloat const kTextSizeBody = 16.0;
static CGFloat const kMarginStandard = 16.0;
static UIColor *kPrimaryColor = [UIColor colorWithRed:0.38 green:0.0 blue:0.93 alpha:1.0];
// 使用
label.font = [UIFont systemFontOfSize:kTextSizeBody];
避坑指南:我曾经因为硬编码了一个颜色值,导致深色模式适配时改了200多个文件。从那以后,所有颜色、尺寸、字符串必须走资源文件或常量。别偷懒,偷懒的代价是加班。
四、网络请求的“游击战”
网络请求散落在各个页面,这是移动端代码最常见的坏味道之一。每个页面都自己创建网络客户端,自己处理回调,自己解析数据……
这样做的问题:
- 无法统一处理错误(比如Token过期)
- 无法统一添加Header(比如设备信息)
- 无法统一做缓存策略
- 测试起来极其痛苦
重构方案:网络层统一封装
// Android:统一网络层
public class ApiClient {
private static ApiClient instance;
private OkHttpClient client;
private ApiClient() {
client = new OkHttpClient.Builder()
.addInterceptor(new AuthInterceptor()) // 统一添加Token
.addInterceptor(new LogInterceptor()) // 统一日志
.connectTimeout(30, TimeUnit.SECONDS)
.build();
}
public static synchronized ApiClient getInstance() {
if (instance == null) {
instance = new ApiClient();
}
return instance;
}
public void get(String url, Callback callback) {
Request request = new Request.Builder()
.url(url)
.header("Content-Type", "application/json")
.build();
client.newCall(request).enqueue(callback);
}
}
// 使用:所有页面都通过ApiClient发起请求
ApiClient.getInstance().get("https://api.example.com/data", new Callback() {
// 统一处理成功/失败
});
核心思想:网络层应该像水龙头一样,打开就有水,关了就停。页面不需要知道水是从哪里来的,只需要知道怎么用。
五、UI与业务逻辑的“纠缠”
这个坏味道在移动端特别常见。比如在ListView的Adapter里直接写网络请求,或者在TableView的Cell里调用API。你想想看,一个列表项居然要自己去拉数据,这合理吗?
我见过最离谱的代码:一个RecyclerView的Adapter里,onBindViewHolder方法中直接new了一个网络请求,然后更新当前Item的UI。这样做的问题在于:
- Adapter职责过重
- 网络请求无法复用
- 列表滑动时频繁触发网络请求
- 内存泄漏风险高
正确的做法:数据驱动UI
// 重构前:Adapter里直接请求网络
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// 直接在这里请求网络
OkHttpClient client = new OkHttpClient();
// ... 网络请求代码
}
}
// 重构后:数据准备好再传给Adapter
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private List<DataItem> dataList;
public void setData(List<DataItem> data) {
this.dataList = data;
notifyDataSetChanged();
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
DataItem item = dataList.get(position);
holder.textView.setText(item.getTitle());
// 只做UI绑定,不做网络请求
}
}
// ViewModel或Presenter负责准备数据
viewModel.loadData().observe(this, data -> {
adapter.setData(data);
});
六、线程管理的“野路子”
移动端开发,线程管理是个大坑。Android里有人到处new Thread(),iOS里有人直接在主线程做耗时操作。结果就是:App卡顿、ANR、崩溃。
我个人习惯:统一线程池,明确线程职责
| 操作类型 | Android | iOS |
|---|---|---|
| UI更新 | 主线程 | 主线程 |
| 网络请求 | 子线程(OkHttp自带) | 子线程(URLSession自带) |
| 数据库操作 | 子线程(Room自带) | 子线程(CoreData自带) |
| 图片加载 | 子线程(Glide自带) | 子线程(SDWebImage自带) |
| 复杂计算 | 线程池 | GCD或OperationQueue |
// Android:统一线程池
public class AppExecutors {
private static final int THREAD_COUNT = 4;
private final Executor diskIO;
private final Executor networkIO;
private final Executor mainThread;
private AppExecutors() {
this.diskIO = Executors.newSingleThreadExecutor();
this.networkIO = Executors.newFixedThreadPool(THREAD_COUNT);
this.mainThread = new MainThreadExecutor();
}
// 使用
appExecutors.networkIO.execute(() -> {
// 网络请求
appExecutors.mainThread.execute(() -> {
// 更新UI
});
});
}
// iOS:统一线程管理
dispatch_queue_t networkQueue = dispatch_queue_create("com.app.network", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t diskQueue = dispatch_queue_create("com.app.disk", DISPATCH_QUEUE_SERIAL);
dispatch_async(networkQueue, ^{
// 网络请求
dispatch_async(dispatch_get_main_queue(), ^{
// 更新UI
});
});
我的建议:能用框架自带的线程管理就用框架。比如Android的ViewModel + LiveData,iOS的Combine或RxSwift。它们已经帮你处理好了线程切换,别自己再造轮子。
七、知识体系总览
下面这张图总结了移动端重构的核心逻辑,我把它叫做“移动端代码清洁金字塔”:
这个金字塔的核心逻辑是:上层依赖下层,下层不依赖上层。UI层只知道业务逻辑层的接口,业务逻辑层只知道基础设施层的接口。这样改UI不会影响业务逻辑,换网络库不会影响UI。
八、总结
移动端重构,说白了就是三件事:拆分、统一、解耦。
- 拆分:把巨型Activity/ViewController拆成多个小文件
- 统一:网络层、线程池、资源管理统一封装
- 解耦:UI和业务逻辑分离,数据和展示分离
嗯,做到这三点,你的移动端代码基本就告别“坏味道”了。剩下的就是持续重构,保持代码整洁。毕竟,代码就像房间,不打扫就会越来越乱。