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。它们已经帮你处理好了线程切换,别自己再造轮子。

七、知识体系总览

下面这张图总结了移动端重构的核心逻辑,我把它叫做“移动端代码清洁金字塔”:

移动端代码清洁金字塔 基础设施层:统一网络层、统一线程池、统一资源管理 业务逻辑层:ViewModel/Presenter、Repository、UseCase UI层:Activity/ViewController、Fragment/View 目标:可维护、可测试、高性能 依赖方向 职责清晰

这个金字塔的核心逻辑是:上层依赖下层,下层不依赖上层。UI层只知道业务逻辑层的接口,业务逻辑层只知道基础设施层的接口。这样改UI不会影响业务逻辑,换网络库不会影响UI。

八、总结

移动端重构,说白了就是三件事:拆分、统一、解耦

  • 拆分:把巨型Activity/ViewController拆成多个小文件
  • 统一:网络层、线程池、资源管理统一封装
  • 解耦:UI和业务逻辑分离,数据和展示分离

嗯,做到这三点,你的移动端代码基本就告别“坏味道”了。剩下的就是持续重构,保持代码整洁。毕竟,代码就像房间,不打扫就会越来越乱。


公众号:蓝海资料掘金营,微信deep3321