android okhttp+RxJava+retrofit网络请求框架用法解析 使用举例

前言

    这个网络请求框架是我来之前同事引入公司项目的,使用的是MVP模式,因为之前用的是公司自己的网络请求架构,所以熟悉它的用法费了我不少时间,但是用习惯之后发现这个框架写网络请求很方便,因为它封装的很好,而且这个框架也是目前android开发比较主流的网络请求框架,所以写一篇文章记录一下使用这个框架的大致流程,因为以后很有可能再用到。

正文

1.添加项目依赖库:

    //components
    implementation 'com.android.support:design:28.0.0'
    implementation 'com.google.code.gson:gson:2.8.5'
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'com.blankj:utilcode:1.23.7'
    //rx
    implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
    //network
    implementation 'com.google.code.gson:gson:2.8.5'
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
    implementation 'com.github.bumptech.glide:glide:4.8.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
    implementation('com.github.ihsanbal:LoggingInterceptor:3.0.0') {
        exclude group: 'org.json', module: 'json'
    }
    //di
    implementation 'com.google.dagger:dagger:2.17'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.17'
    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
    implementation 'com.android.support:multidex:1.0.3'
    implementation 'me.yokeyword:fragmentation:1.3.3'
    implementation 'com.orhanobut:logger:2.1.1'

工程目录下build目录修改如下:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    
    repositories {
        google()
        maven { url "https://dl.bintray.com/thelasterstar/maven/" }
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
        mavenCentral()
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

2.初始化okhttp设置:

@Module
public class HttpModule {

    @Singleton
    @Provides
    Retrofit.Builder provideRetrofitBuilder() {
        return new Retrofit.Builder();
    }


    @Singleton
    @Provides
    OkHttpClient.Builder provideOkHttpBuilder() {
        return new OkHttpClient.Builder();
    }

    @Singleton
    @Provides
    @MainUrl
    Retrofit provideZhihuRetrofit(Retrofit.Builder builder, OkHttpClient client) {
        return createRetrofit(builder, client, Constant.BASE_HOST);
    }

    @Singleton
    @Provides
    OkHttpClient provideClient(OkHttpClient.Builder builder) {
        if (BuildConfig.DEBUG) {

            LoggingInterceptor httpLoggingInterceptor = new LoggingInterceptor.Builder()
                    .loggable(BuildConfig.DEBUG)
                    .setLevel(Level.BASIC)
                    .log(Platform.INFO)
                    .request("Request")
                    .response("Response")
                    .build();
//            HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
//            loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
            builder.addInterceptor(httpLoggingInterceptor);
        }
        File cacheFile = new File(Constant.PATH_CACHE);
        Cache cache = new Cache(cacheFile, 1024 * 1024 * 50);
        Interceptor cacheInterceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                if (!SystemUtil.isNetworkConnected()) {
                    request = request.newBuilder()
                            .cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }
                Response response = chain.proceed(request);
                if (SystemUtil.isNetworkConnected()) {
                    int maxAge = 0;
                    // 有网络时, 不缓存, 最大保存时长为0
                    response.newBuilder()
                            .header("Cache-Control", "public, max-age=" + maxAge)
                            .removeHeader("Pragma")
                            .build();
                } else {
                    // 无网络时,设置超时为4周
                    int maxStale = 60 * 60 * 24 * 28;
                    response.newBuilder()
                            .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                            .removeHeader("Pragma")
                            .build();
                }
                return response;
            }
        };
//        Interceptor apikey = new Interceptor() {
//            @Override
//            public Response intercept(Chain chain) throws IOException {
//                Request request = chain.request();
//                request = request.newBuilder()
//                        .addHeader("apikey",Constants.KEY_API)
//                        .build();
//                return chain.proceed(request);
//            }
//        }
//        设置统一的请求头部参数
//        builder.addInterceptor(apikey);
        //设置缓存
        builder.addNetworkInterceptor(cacheInterceptor);
        builder.addInterceptor(cacheInterceptor);
        builder.cache(cache);
        //设置超时
        builder.connectTimeout(10, TimeUnit.SECONDS);
        builder.readTimeout(20, TimeUnit.SECONDS);
        builder.writeTimeout(20, TimeUnit.SECONDS);
        //错误重连
        builder.retryOnConnectionFailure(true);
        return builder.build();
    }

    @Singleton
    @Provides
    MainApi provideMainService(@MainUrl Retrofit retrofit) {
        return retrofit.create(MainApi.class);
    }

    private Retrofit createRetrofit(Retrofit.Builder builder, OkHttpClient client, String url) {
        return builder
                .baseUrl(url)
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }
}

细节解析:

1.该类中使用到的一些标签不要遗漏,尤其是类名上的@Module标签;

2.Constant.BASE_HOST--使用你的接口DoMain,比如你的接口请求完整地址是https://www.baidu.com/v2/draft/add,那么这个参数应该填入https://www.baidu.com/;

3.按照上面的写法,请求和返回的相关数据都会打印到日志里,需要自定义打印日志信息的话自行修改provideClient方法中的httpLoggingInterceptor;

4.provideMainService中的MainApi.class是你的接口声明类,下一步会提到。

2.创建接口声明类MainApi

public interface MainApi {
    @POST("courses/index/")
    Flowable<EditFrameInfo> getUserInfo();

    @Multipart
    @POST("v2/draft/add")
    Flowable<PictureBean> saveCoursewareInfo(@PartMap Map<String, RequestBody> map);

    @GET("attach/materials")
    Flowable<ResourcesBean> getResources(@Query("type") String type);
}

这里我选择的代码把常用的几种请求方式都涵盖了,第一种是不需要参数的post请求,第二种是需要参数的post请求,第三种是带参数的get请求,根据你的请求类型自行选用即可。这里如果你是post的带参数请求,可以统一把参数格式都写成@PartMap Map<String, RequestBody> map这样。

细节解析:

1.@POST("v2/draft/add")--括号里的是你想要调用的地址domain后面的部分。

3.实现MainApi中的接口

1.定义HttpHelper接口:

public interface HttpHelper {
    Flowable<PictureBean> saveCoursewareInfo(@PartMap Map<String, RequestBody> map);
}

这里直接把MainApi里定义的接口除了标签部分复制进来就可以了。

扫描二维码关注公众号,回复: 11066743 查看本文章

2.实现HttpHelper接口:

public class RetrofitHelper implements HttpHelper {
    private MainApi mMainApiService;

    @Inject
    public RetrofitHelper(MainApi mMainApiService) {
        this.mMainApiService = mMainApiService;
    }

    @Override
    public Flowable<PictureBean> saveCoursewareInfo(Map<String, RequestBody> map) {
        return mMainApiService.saveCoursewareInfo(map);
    }
}

注意这里的@Inject别忘记加。

3.创建DataManager类:

该类用于管理所有的数据请求,就是封装了一层方法调用。

public class DataManager implements HttpHelper {
    HttpHelper mHttpHelper;

    public DataManager(HttpHelper httpHelper) {
        mHttpHelper = httpHelper;
    }

    @Override
    public Flowable<PictureBean> saveCoursewareInfo(Map<String, RequestBody> map) {
        return mHttpHelper.saveCoursewareInfo(map);
    }
}

4.定义Contract接口:

public interface MainContract {
    interface View extends BaseView {
        void responseCoursewareId(int id);
    }

    interface Presenter extends BasePresenter<View> {
        void saveCoursewareLib(Map<String, RequestBody> map);
    }
}

Presenter接口定义请求方法,View接口定义回调方法。简单来说就是你想调用MainApi里定义的接口就用Presenter里的方法去请求,请求之后的会回调View里对应的方法。

5.定义Presenter:

public class MainPresenter extends RxPresenter<MainContract.View> implements MainContract.Presenter {

    private static final String TAG = MainPresenter.class.getSimpleName();

    DataManager dataManager;

    @Inject
    public MainPresenter(DataManager dataManager) {
        this.dataManager = dataManager;
    }

    @Override
    public void saveCoursewareLib(Map<String, RequestBody> map) {
        addSubscribe(dataManager.saveCoursewareInfo(map)
                .compose(RxUtil.rxSchedulerHelper())
                .subscribeWith(new CommonSubscriber<PictureBean>(mView) {
                    @Override
                    public void onNext(PictureBean pictureBean) {
                        if (pictureBean.getData() != null) {
                            if (pictureBean.getData().getCode() == Constant.NET_REQUEST_OK && mView != null) {
                                mView.responseCoursewareId(pictureBean.getData().getCourseId());
                                ToastUtils.showShort(pictureBean.getMsg());
                            } else {
                                LogUtil.e(TAG + " request fail or mView is null");
                            }
                        } else {
                            ToastUtils.showShort(pictureBean.getMsg());
                        }
                    }
                }));
    }
}

Presenter实现Contract里的接口,记住不要遗漏构造函数上方的@Injeft标签。

除了onNext方法以外,还有

                               @Override
                               public void onError(Throwable e) {
                                   super.onError(e);
                               }

                               @Override
                               protected void onStart() {
                                   super.onStart();
                               }

                               @Override
                               public void onComplete() {
                                   super.onComplete();
                               }

根据自己需求自行添加。

细节解析:

1.onNext返回的参数PictureBean是用来解析这个接口返回的数据写的实体类,框架会自动将接口返回的数据按照这个实体类去解析,该实体类根据自己的接口返回数据去写,如果你的AS安装了GsonFormat插件,使用插件直接生成一个实体类就行了。

附上代码中用到的实体类代码作为参考:

public class PictureBean {

    /**
     * errorCode : 0
     * msg : success
     * data : {"id":1,"url":"asadsa"}
     */

    private int errorCode;
    private String msg;
    private DataBean data;

    public int getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(int errorCode) {
        this.errorCode = errorCode;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public DataBean getData() {
        return data;
    }

    public void setData(DataBean data) {
        this.data = data;
    }

    public static class DataBean {
        /**
         * id : 1
         * url : asadsa
         */

        private int imgId;
        private String url;
        private int code;
        private int chunk;
        private int courseId;
        private int actualNum;
        private int lastChunk;

        public int getImgId() {
            return imgId;
        }

        public void setImgId(int imgId) {
            this.imgId = imgId;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public int getCode() {
            return code;
        }

        public void setCode(int code) {
            this.code = code;
        }

        public int getChunk() {
            return chunk;
        }

        public void setChunk(int chunk) {
            this.chunk = chunk;
        }

        public int getCourseId() {
            return courseId;
        }

        public void setCourseId(int courseId) {
            this.courseId = courseId;
        }

        public int getActualNum() {
            return actualNum;
        }

        public void setActualNum(int actualNum) {
            this.actualNum = actualNum;
        }

        public int getLastChunk() {
            return lastChunk;
        }

        public void setLastChunk(int lastChunk) {
            this.lastChunk = lastChunk;
        }

        @Override
        public String toString() {
            return "DataBean{" +

                    "imgId=" + imgId +
                    ", url='" + url + '\'' +
                    ", code=" + code +
                    ", chunk=" + chunk +
                    ", courseId=" + courseId +
                    ", actualNum=" + actualNum +
                    ", lastChunk=" + lastChunk +
                    '}';
        }
    }

    @Override
    public String toString() {
        return "PictureBean{" +
                "errorCode=" + errorCode +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }
}

6.在代码中使用

这里需要提到几个基类,Activity基类和Fragment基类:

1.SimpleActivity


/**
 * Created by codeest on 16/8/11.
 * 无MVP的activity基类
 */

public abstract class SimpleActivity extends SupportActivity {

    protected Activity mContext;
    private Unbinder mUnBinder;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayout());
        mUnBinder = ButterKnife.bind(this);
        mContext = this;
        if(Utils.checkLogin(mContext)){
            onViewCreated();
            App.getInstance().addActivity(this);
            initEventAndData();
        }
    }

    protected void setToolBar(Toolbar toolbar, String title) {
        toolbar.setTitle(title);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setDisplayShowHomeEnabled(true);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                onBackPressedSupport();
            }
        });
    }

    protected void onViewCreated() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        App.getInstance().removeActivity(this);
        mUnBinder.unbind();
        System.gc();
    }

    protected abstract int getLayout();

    protected abstract void initEventAndData();
}

项目中所有Activity的基类,如果不涉及到网络请求,就继承这个,如果涉及到网络请求则继承下面提到的这个。

2.BaseActivity

/**
 * Created by codeest on 2016/8/2.
 * MVP activity基类
 */
public abstract class BaseActivity<T extends BasePresenter> extends SimpleActivity implements BaseView {

    @Inject
    protected T mPresenter;

    protected LoadingUtil mLoading;

    protected ActivityComponent getActivityComponent() {
        return DaggerActivityComponent.builder()
                .appComponent(App.getAppComponent())
                .activityModule(getActivityModule())
                .build();
    }

    protected ActivityModule getActivityModule() {
        return new ActivityModule(this);
    }

    @Override
    protected void onViewCreated() {
        super.onViewCreated();
        initInject();

        mLoading = LoadingUtil.getInstance(new ProgressDialogHandler(this, new ProgressCancelListener() {
            @Override
            public void onCancelProgress() {
                //等待框显示时 取消监听
            }

            @Override
            public void onCancelDispose() {
                //等待框显示时 返回键监听
            }
        }, false));
        if (mPresenter != null)
            mPresenter.attachView(this);
    }

    @Override
    protected void onDestroy() {
        if (mPresenter != null)
            mPresenter.detachView();
        super.onDestroy();
    }

    @Override
    public void showErrorMsg(String msg) {
        SnackbarUtil.show(((ViewGroup) findViewById(android.R.id.content)).getChildAt(0), msg);
    }

//    @Override
//    public void useNightMode(boolean isNight) {
//        if (isNight) {
//            AppCompatDelegate.setDefaultNightMode(
//                    AppCompatDelegate.MODE_NIGHT_YES);
//        } else {
//            AppCompatDelegate.setDefaultNightMode(
//                    AppCompatDelegate.MODE_NIGHT_NO);
//        }
//        recreate();
//    }

    @Override
    public void stateError() {

    }

    @Override
    public void stateEmpty() {

    }

    @Override
    public void stateLoading() {

    }

    @Override
    public void stateMain() {

    }

    protected abstract void initInject();
}

3.SimpleFragment


/**
 * Created by codeest on 16/8/11.
 * 无MVP的Fragment基类
 */

public abstract class SimpleFragment extends SupportFragment {

    protected View mView;
    protected Activity mActivity;
    protected Context mContext;
    private Unbinder mUnBinder;
    protected boolean isInited = false;

    @Override
    public void onAttach(Context context) {
        mActivity = (Activity) context;
        mContext = context;
        super.onAttach(context);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mView = inflater.inflate(getLayoutId(), null);
        return mView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mUnBinder = ButterKnife.bind(this, view);
    }

    @Override
    public void onLazyInitView(@Nullable Bundle savedInstanceState) {
        super.onLazyInitView(savedInstanceState);
        isInited = true;
        initEventAndData();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        mUnBinder.unbind();
    }

    protected abstract int getLayoutId();
    protected abstract void initEventAndData();
}

跟Activity一样。

4.BaseFragment

/**
 * Created by codeest on 2016/8/2.
 * MVP Fragment基类
 */
public abstract class BaseFragment<T extends BasePresenter> extends SimpleFragment implements BaseView {

    @Inject
    protected T mPresenter;

    protected FragmentComponent getFragmentComponent(){
        return DaggerFragmentComponent.builder()
                .appComponent(App.getAppComponent())
                .fragmentModule(getFragmentModule())
                .build();
    }

    protected FragmentModule getFragmentModule(){
        return new FragmentModule(this);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        initInject();
        mPresenter.attachView(this);
        super.onViewCreated(view, savedInstanceState);
    }

    @Override
    public void onDestroyView() {
        if (mPresenter != null) mPresenter.detachView();
        super.onDestroyView();
    }

    @Override
    public void showErrorMsg(String msg) {
        SnackbarUtil.show(((ViewGroup) getActivity().findViewById(android.R.id.content)).getChildAt(0), msg);
    }

//    @Override
//    public void useNightMode(boolean isNight) {
//
//    }

    @Override
    public void stateError() {

    }

    @Override
    public void stateEmpty() {

    }

    @Override
    public void stateLoading() {

    }

    @Override
    public void stateMain() {

    }

    protected abstract void initInject();
}

5.在Activity中进行网络请求:

public class MainActivity extends BaseActivity<MainPresenter> implements MainContract.View{

    @Override
    protected void initInject() {
        getActivityComponent().inject(this);
    }

    @Override
    protected int getLayout() {
        return R.layout.activity_main;
    }

    @Override
    protected void initEventAndData() {
        int mills = Utils.getCurrentTimeMills();
        Map<String, RequestBody> map = new HashMap<>();
        if (course_id != 0) {
            map.put("course_id", MultipartBody.create(MultipartBody.FORM, String.valueOf(course_id)));
        }
        map.put("user_id", MultipartBody.create(MultipartBody.FORM, String.valueOf(SharedPreferencesUtil.getIntDate(Constant.SPKEY_USERID))));
        map.put("title", MultipartBody.create(MultipartBody.FORM, ""));
        map.put("content", MultipartBody.create(MultipartBody.FORM, new Gson().toJson(mCoursewareInfo)));
        map.put("timestamp", MultipartBody.create(MultipartBody.FORM, mills + ""));
        map.put("token", MultipartBody.create(MultipartBody.FORM, MD5Utils.md5(String.valueOf(SharedPreferencesUtil.getIntDate(Constant.SPKEY_USERID)) + mills + Constant.KEY)));
        mPresenter.saveCoursewareLib(map);
    }
    
    @Override
    public void responseCoursewareId(int id) {
        //saveCoursewareLib的请求回调
    }

}

这里用请求saveCoursewareLib方法举例,就是我们上面有过声明的一个请求方法。这里有个带有标签@Inject的方法,里面有一句getActivityComponent().inject(this); 这个方法的调用就需要我们配置Activity组件了,另外会附上Fragment组件的配置代码。

6.Activity组件配置:

@ActivityScope
@Component(dependencies = AppComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {

    Activity getActivity();

    void inject(MainActivity mainActivity);
}

//移到项目中请另外创建一个类,文章中偷懒写在一起了,并不是上面的内部类
@Module
public class ActivityModule {
    private Activity mActivity;

    public ActivityModule(Activity activity) {
        this.mActivity = activity;
    }

    @Provides
    @ActivityScope
    public Activity provideActivity() {
        return mActivity;
    }
}

7.Fragment组件配置:

@FragmentScope
@Component(dependencies = AppComponent.class, modules = FragmentModule.class)
public interface FragmentComponent {

    Activity getActivity();

    void inject(CoursewareDraftFragment coursewareDraftFragment);
}

//跟Activity组件一样,偷懒写在一起了
@Module
public class FragmentModule {

    private Fragment fragment;

    public FragmentModule(Fragment fragment) {
        this.fragment = fragment;
    }

    @Provides
    @FragmentScope
    public Activity provideActivity() {
        return fragment.getActivity();
    }
}

//使用:
    @Override
    protected void initInject() {
        getFragmentComponent().inject(this);
    }

8.附上全局接口回调定义类,兴许你能用得着 

public abstract class CommonSubscriber<T> extends ResourceSubscriber<T> {
    private BaseView mView;
    private String mErrorMsg;
    private boolean isShowErrorState = true;

    protected CommonSubscriber(BaseView view) {
        this.mView = view;
    }

    protected CommonSubscriber(BaseView view, String errorMsg) {
        this.mView = view;
        this.mErrorMsg = errorMsg;
    }

    protected CommonSubscriber(BaseView view, boolean isShowErrorState) {
        this.mView = view;
        this.isShowErrorState = isShowErrorState;
    }

    protected CommonSubscriber(BaseView view, String errorMsg, boolean isShowErrorState) {
        this.mView = view;
        this.mErrorMsg = errorMsg;
        this.isShowErrorState = isShowErrorState;
    }

    @Override
    public void onComplete() {

    }

    @Override
    public void onError(Throwable e) {
        if (mView == null) {
            return;
        }
        if (mErrorMsg != null && !TextUtils.isEmpty(mErrorMsg)) {
            mView.showErrorMsg(mErrorMsg);
        } else if (e instanceof ApiException) {
            mView.showErrorMsg(e.getMessage());
        } else if (e instanceof HttpException) {
            if (NetworkUtils.isConnected())
                mView.showErrorMsg("数据请求异常ヽ(≧Д≦)ノ");
            else
                mView.showErrorMsg("网络异常,请检查网络是否连接ヽ(≧Д≦)ノ");
        } else if (e instanceof SocketTimeoutException) {
            mView.showErrorMsg("网络不佳,请重新加载!ヽ(≧Д≦)ノ");
        } else if (e instanceof JsonParseException) {
            mView.showErrorMsg("数据解析错误ヽ(≧Д≦)ノ");
        } else {
            mView.showErrorMsg("未知错误ヽ(≧Д≦)ノ");
            LogUtil.d(e.toString());
        }
        if (isShowErrorState) {
            mView.stateError();
        }
        e.printStackTrace();
    }
}

结语

    一个流程走下来,涉及到的东西挺多的,所以刚接触的话可能会有点摸不着北,其实每个接口的定义和使用都是这么一套东西,可以试着自己走一两个接口的全流程,走几遍之后流程熟悉了就好了。熟悉之后你会发现这个框架用起来还是很方便的。

    如果这篇文章对你有帮助,点一下免费的赞和关注吧,这些是我更博的动力~

    如果遇到问题,欢迎下方留言。

发布了73 篇原创文章 · 获赞 30 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/yonghuming_jesse/article/details/104812736