Retrofit 2简单使用教程

作者: Chike Mgbemena
原文地址:https://code.tutsplus.com/tutorials/getting-started-with-retrofit-2–cms-27792

PS:我想先说一下其实我翻译这篇文章的动机就是想锻炼一下自己的翻译能力,然后也可以作为一个起点,希望以后能阅读和翻译更多的英文文章!

下面是原文~


最后你的项目会是这个样子

什么是Retrofit?

Retrofit是Android和Java中一种类型安全的HTTP客户端。Retrofit通过将API转换为Java接口从而使连接网络服务变得简单。在这个教程中,我会向你展示这个Android中最受欢迎而且经常被提及的HTTP库之一。

这个强大的库通过将JSON和XML数据转换为普通的Java对象(POJOs)从而使得解析变得简单。GET、POST、PUT、PATCH和DELETE请求都可以被执行。

像大多数的开源软件一样,Retrofit也是建立在一些其它强大的库和工具的基础之上。在底层中,Retrofit使用OKHttp(它们的作者相同)来处理网络请求,而且Retrofit内没有集成任何将JSON转换为Java对象的JSON转换器。但是它支持以下的JSON 转换库来处理这些需求:

  • GSON: com.squareup.retrofit:converter-gson
  • Jackson: com.squareup.retrofit:converter-jackson
  • Moshi: com.squareup.retrofit:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire

对于XML,Retrofit支持

  • Simple FrameWork: com.squareup.retrofit2:converter-simpleframework

为什么要使用Retrofit?

如果你自己实现一个类型安全的HTTP库来与数据接口连接的话会是非常痛苦的:你需要处理各种各样的功能点,比如实现连接、缓存、失败重连、多线程、结果解析、错误处理等等。从另一方面来说,Retrofit是一个设计良好、规范而且经过严格测试的库,它会帮你节省大量宝贵的时间并能减少你的痛苦。

在本教程中,我会通过实现一个简单的从Stack Exchange API查询最近的答案
的app来指导你如何使用Retrofit 2来处理网络请求。我们会使用GET方式来请求网络,即将一个端点-/answers添加到基础URLhttps://api.stackexchange.com/2.2/之后,然后得到结果并显示在RecyclerView中。我还会向你展示如何与RxJava一同使用来简化对状态和数据的管理。

1.创建一个Android Studio 项目

打开AS并创建一个新的项目,项目中包含一个空的MainActivity。

2.声明依赖

创建完项目之后,在你的build.gradle中声明以下的依赖。这些依赖包括recycler view、Retrofit库、Google的Gson以及Retrofit的Gson集成。

// Retrofit
compile 'com.squareup.retrofit2:retrofit:2.1.0'

// JSON Parsing
compile 'com.google.code.gson:gson:2.6.1'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

// recyclerview 
compile 'com.android.support:recyclerview-v7:25.0.1'

添加后不要忘记点击同步来下载这些库。

3.添加网络权限

要完成网络请求,我们需要在AndroidManifest.xml文件中添加INTERNET权限

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.chikeandroid.retrofittutorial">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

4.自动生成模型

我们将会使用一个非常有用的工具 jsonschema2pojo来根据返回的JSON数据自动创建模型。

获取Json样例数据

复制并粘贴https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow到你的浏览器地址栏中(如果你对Postman熟悉也可以使用它)。然后按下回车——这样就会根据给定的端点执行一次GET请求。你将会看到的是一个JSON对象的数据。下面的截图是通过Postman得到的JSON数据。

    {
  "items": [
    {
      "owner": {
        "reputation": 1,
        "user_id": 6540831,
        "user_type": "registered",
        "profile_image": "https://www.gravatar.com/avatar/6a468ce8a8ff42c17923a6009ab77723?s=128&d=identicon&r=PG&f=1",
        "display_name": "bobolafrite",
        "link": "http://stackoverflow.com/users/6540831/bobolafrite"
      },
      "is_accepted": false,
      "score": 0,
      "last_activity_date": 1480862271,
      "creation_date": 1480862271,
      "answer_id": 40959732,
      "question_id": 35931342
    },
    {
      "owner": {
        "reputation": 629,
        "user_id": 3054722,
        "user_type": "registered",
        "profile_image": "https://www.gravatar.com/avatar/0cf65651ae9a3ba2858ef0d0a7dbf900?s=128&d=identicon&r=PG&f=1",
        "display_name": "jeremy-denis",
        "link": "http://stackoverflow.com/users/3054722/jeremy-denis"
      },
      "is_accepted": false,
      "score": 0,
      "last_activity_date": 1480862260,
      "creation_date": 1480862260,
      "answer_id": 40959731,
      "question_id": 40959661
    },
    ...
  ],
  "has_more": true,
  "backoff": 10,
  "quota_max": 300,
  "quota_remaining": 241
}  

从浏览器或者Postman复制上面的JSON数据。

将JSON数据映射为Java类

现在访问 jsonschema2pojo并将JSON数据粘贴到输入框中。

选择source type为JSON,annotation style为Gson,并取消选中
Allow additional properties

然后点击Preview按钮来生成Java对象。

你或许会好奇生成的代码中@SerializedName 和 @Expose两个注解是干什么的。别担心,我会详细解释的。

@SerializedName注解是Gson在将JSON的key映射为类的属性时用到的。为了保持Java的驼峰命名习惯,并不推荐使用下划线分割单词来为变量命名。@SerializedName可以帮助我们在这两者之间进行转换。

@SerializedName("quota_remaining")
@Expose
private Integer quotaRemaining;

在上面的例子中,我们告诉Gson JSON数据中的键quota_remaining应该被映射为Java的属性quotaRemaining。如果它们两个是相同的,例如,如果我们的JSON中的键是quotaRemaining,那么我们就不需要@SerializedName注解,应为Gson会自动映射它们。

@Expose注解表示这个成员在序列化和反序列化时都是可见的。

将数据模型导入AS中

现在回到AS中。在main包下新建一个名为data的子包。在新建的data包下,创建一个名为model的包。在model包内,创建一个名为Owner的Java类。现在复制并粘贴通过jsonschema2pojo生成的Owner类到你创建的Owner类中。

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class Owner {

    @SerializedName("reputation")
    @Expose
    private Integer reputation;
    @SerializedName("user_id")
    @Expose
    private Integer userId;
    @SerializedName("user_type")
    @Expose
    private String userType;
    @SerializedName("profile_image")
    @Expose
    private String profileImage;
    @SerializedName("display_name")
    @Expose
    private String displayName;
    @SerializedName("link")
    @Expose
    private String link;
    @SerializedName("accept_rate")
    @Expose
    private Integer acceptRate;


    public Integer getReputation() {
        return reputation;
    }

    public void setReputation(Integer reputation) {
        this.reputation = reputation;
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUserType() {
        return userType;
    }

    public void setUserType(String userType) {
        this.userType = userType;
    }

    public String getProfileImage() {
        return profileImage;
    }

    public void setProfileImage(String profileImage) {
        this.profileImage = profileImage;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public String getLink() {
        return link;
    }

    public void setLink(String link) {
        this.link = link;
    }

    public Integer getAcceptRate() {
        return acceptRate;
    }

    public void setAcceptRate(Integer acceptRate) {
        this.acceptRate = acceptRate;
    }
} 

新建一个Item类,然后步骤和上面相同

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class Item {

    @SerializedName("owner")
    @Expose
    private Owner owner;
    @SerializedName("is_accepted")
    @Expose
    private Boolean isAccepted;
    @SerializedName("score")
    @Expose
    private Integer score;
    @SerializedName("last_activity_date")
    @Expose
    private Integer lastActivityDate;
    @SerializedName("creation_date")
    @Expose
    private Integer creationDate;
    @SerializedName("answer_id")
    @Expose
    private Integer answerId;
    @SerializedName("question_id")
    @Expose
    private Integer questionId;
    @SerializedName("last_edit_date")
    @Expose
    private Integer lastEditDate;

    public Owner getOwner() {
        return owner;
    }

    public void setOwner(Owner owner) {
        this.owner = owner;
    }

    public Boolean getIsAccepted() {
        return isAccepted;
    }

    public void setIsAccepted(Boolean isAccepted) {
        this.isAccepted = isAccepted;
    }

    public Integer getScore() {
        return score;
    }

    public void setScore(Integer score) {
        this.score = score;
    }

    public Integer getLastActivityDate() {
        return lastActivityDate;
    }

    public void setLastActivityDate(Integer lastActivityDate) {
        this.lastActivityDate = lastActivityDate;
    }

    public Integer getCreationDate() {
        return creationDate;
    }

    public void setCreationDate(Integer creationDate) {
        this.creationDate = creationDate;
    }

    public Integer getAnswerId() {
        return answerId;
    }

    public void setAnswerId(Integer answerId) {
        this.answerId = answerId;
    }

    public Integer getQuestionId() {
        return questionId;
    }

    public void setQuestionId(Integer questionId) {
        this.questionId = questionId;
    }

    public Integer getLastEditDate() {
        return lastEditDate;
    }

    public void setLastEditDate(Integer lastEditDate) {
        this.lastEditDate = lastEditDate;
    }
}

最后,新建一个表示返回的StackOverflow的答案的名为SOAnswersResponse的类。这个类中需要的内容就是jsonschema2pojo生成的Example中的内容。确保在所有Example出现的地方替换为SOAnswersResponse。

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.List;

public class SOAnswersResponse {

    @SerializedName("items")
    @Expose
    private List<Item> items = null;
    @SerializedName("has_more")
    @Expose
    private Boolean hasMore;
    @SerializedName("backoff")
    @Expose
    private Integer backoff;
    @SerializedName("quota_max")
    @Expose
    private Integer quotaMax;
    @SerializedName("quota_remaining")
    @Expose
    private Integer quotaRemaining;

    public List<Item> getItems() {
        return items;
    }

    public void setItems(List<Item> items) {
        this.items = items;
    }

    public Boolean getHasMore() {
        return hasMore;
    }

    public void setHasMore(Boolean hasMore) {
        this.hasMore = hasMore;
    }

    public Integer getBackoff() {
        return backoff;
    }

    public void setBackoff(Integer backoff) {
        this.backoff = backoff;
    }

    public Integer getQuotaMax() {
        return quotaMax;
    }

    public void setQuotaMax(Integer quotaMax) {
        this.quotaMax = quotaMax;
    }

    public Integer getQuotaRemaining() {
        return quotaRemaining;
    }

    public void setQuotaRemaining(Integer quotaRemaining) {
        this.quotaRemaining = quotaRemaining;
    }
}

5.创建Retrofit实例

要使用Retrofit对数据接口发起网络请求,我们需要使用Retrofit.Builder来创建一个实例并使用一个基础URL来配置它。

在data包下新建一个remote包,在该包下新建一个类RetrofitClient,这个类用来创建一个Retrofit的单例。创建实例时需要一个基础URL,所以我们会在调用RetrofitClient.getClient(String baseUrl)时传入URL。同时我们也声明了用到的JSON转换器(Gson)。

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitClient {

    private static Retrofit retrofit = null;

    public static Retrofit getClient(String baseUrl) {
        if (retrofit==null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(baseUrl)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

6.创建API接口

在remote包中,新建一个SOService接口。这个接口中会包含一些将会执行HTTP请求的方法,比图GETPOST, PUT, PATCH, 和 DELETE请求等。在这个教程中,我们将会使用GET请求来演示。

import com.chikeandroid.retrofittutorial.data.model.SOAnswersResponse;

import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;

public interface SOService {

   @GET("/answers?order=desc&sort=activity&site=stackoverflow")
   Call<List<SOAnswersResponse>> getAnswers(); 

   @GET("/answers?order=desc&sort=activity&site=stackoverflow")
   Call<List<SOAnswersResponse>> getAnswers(@Query("tagged") String tags);
}

PS:在实现demo时我发现这里应该是

   @GET("/answers?order=desc&sort=activity&site=stackoverflow")
   Call<SOAnswersResponse> getAnswers(); 

   @GET("/answers?order=desc&sort=activity&site=stackoverflow")
   Call<SOAnswersResponse> getAnswers(@Query("tagged") String tags);

@GET注解明确的定义了当该方法调用时就会执行GET请求。在这个接口中的所有方法都必须有一个HTTP注解来提供请求的方式和关联的URL。这里有五种内置的注解可以使用:@GET, @POST, @PUT, @DELETE, 和 @HEAD

在第二个方法的定义中,我们添加了一个查询参数来从服务器过滤数据。Retrofit有一个@Query(“key”)注解来代替请求端点中的硬编码。key表示的是URL中的参数名,Retrofit会自动的把它加到URL后面。例如,如果我们把”android”作为参数传给getAnswers(String tags)方法,那么完整的URL就会是下面这样:

https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow&tagged=android

接口中的方法的参数可以使用下面的几种注解(未翻译)

  • @Path : variable substitution for the API endpoint
  • @Query : specifies the query key name with the value of the annotated parameter
  • @Body : payload for the POST call
  • @Header : specifies the header with the value of the annotated parameter

7.创建API工具类

现在我们将要创建一个ApiUtils工具类。这个类会包含一个静态的变量BASE_URL并且会通过静态的getOService()方法来得到SOService接口。

public class ApiUtils {

    public static final String BASE_URL = "https://api.stackexchange.com/2.2";

    public static SOService getSOService() {
        return RetrofitClient.getClient(BASE_URL).create(SOService.class);
    }
}   

8.显示到RecyclerView中

既然结果要显示到RecyclerView中,那么我们就需要一个适配器。下面就是AnswersAdapter类的内容。

    public class AnswersAdapter extends RecyclerView.Adapter<AnswersAdapter.ViewHolder> {

    private List<Item> mItems;
    private Context mContext;
    private PostItemListener mItemListener;

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{

        public TextView titleTv;
        PostItemListener mItemListener;

        public ViewHolder(View itemView, PostItemListener postItemListener) {
            super(itemView);
            titleTv = (TextView) itemView.findViewById(android.R.id.text1);

            this.mItemListener = postItemListener;
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            Item item = getItem(getAdapterPosition());
            this.mItemListener.onPostClick(item.getAnswerId());

            notifyDataSetChanged();
        }
    }

    public AnswersAdapter(Context context, List<Item> posts, PostItemListener itemListener) {
        mItems = posts;
        mContext = context;
        mItemListener = itemListener;
    }

    @Override
    public AnswersAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        Context context = parent.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);

        View postView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);

        ViewHolder viewHolder = new ViewHolder(postView, this.mItemListener);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(AnswersAdapter.ViewHolder holder, int position) {

        Item item = mItems.get(position);
        TextView textView = holder.titleTv;
        textView.setText(item.getOwner().getDisplayName());
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    public void updateAnswers(List<Item> items) {
        mItems = items;
        notifyDataSetChanged();
    }

    private Item getItem(int adapterPosition) {
        return mItems.get(adapterPosition);
    }

    public interface PostItemListener {
        void onPostClick(long id);
    }
}

9.执行请求

在MainActivity的onCreate方法中,我们对SOService实例、recycler view和适配器进行了初始化,最后我们调用loadAnswers()方法。

private AnswersAdapter mAdapter;
   private RecyclerView mRecyclerView;
   private SOService mService;

   @Override
   protected void onCreate (Bundle savedInstanceState)  {
       super.onCreate( savedInstanceState );
       setContentView(R.layout.activity_main );
       mService = ApiUtils.getSOService();
       mRecyclerView = (RecyclerView) findViewById(R.id.rv_answers);
       mAdapter = new AnswersAdapter(this, new ArrayList<Item>(0), new AnswersAdapter.PostItemListener() {

           @Override
           public void onPostClick(long id) {
               Toast.makeText(MainActivity.this, "Post id is" + id, Toast.LENGTH_SHORT).show();
           }
       });

       RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
       mRecyclerView.setLayoutManager(layoutManager);
       mRecyclerView.setAdapter(mAdapter);
       mRecyclerView.setHasFixedSize(true);
       RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);
       mRecyclerView.addItemDecoration(itemDecoration);

       loadAnswers();
   }

loadAnswers方法通过调用enqueue进行一次网络请求。当结果返回时,Retrofit帮我们把JSON转换为Java对象。(这是通过GsonConverter来实现的)。

    public void loadAnswers() {
    mService.getAnswers().enqueue(new Callback<SOAnswersResponse>() {
    @Override
    public void onResponse(Call<SOAnswersResponse> call, Response<SOAnswersResponse> response) {

        if(response.isSuccessful()) {
            mAdapter.updateAnswers(response.body().getItems());
            Log.d("MainActivity", "posts loaded from API");
        }else {
            int statusCode  = response.code();
            // handle request errors depending on status code
        }
    }

    @Override
    public void onFailure(Call<SOAnswersResponse> call, Throwable t) {
       showErrorMessage();
        Log.d("MainActivity", "error loading from API");

    }
});
}

10.理解 enqueue()

enqueue()方法会异步的发送请求然后在结果返回时通过回调来通知你的app。由于请求是异步的,Retrofit会在子线程中处理它,所以UI线程并不会受到妨碍或被阻塞。

使用enqueue(),你需要实现两个回调方法

  • onResponse()
  • onFailure()

回调时这两个方法中只有一个会被调用。

  • onResponse(): 接受到HTTP返回结果时被调用。它会在返回的结果能被正确的处理时被调用,即使服务器返回的是错误的细腻,比如你得到的返回码是404或者500时,这个方法仍然会被调用。你可以通过response.code()来得到结果的状态码,从而判断返回结果。你也可以使用isSuccessful()方法来判断状态码是否在200-300之间,即表示请求成功。
  • onFailure():在连接服务器发送网络异常或者处理请求发生了意外的异常或者处理返回结果出现异常时会被调用。

你可以通过execute()方法来实现同步的请求。但这样会阻塞UI线程中的用户交互。所以不要在UI线程中执行同步的方法!你要在子线程中运行它们。

11.测试

现在你可以运行app了。

12.集成RxJava

如果你是RxJava的粉丝的话,那么你可以轻松的通过RxJava来实现Retrofit。Retrofit 1是默认集成RxJava的,但是在Retrofit 2中需要包含一些额外的依赖。Retrofit 有一个默认的适配器来执行Call 实例。所以你可以通过包含RxJava的CallAdapter来改变Retrofit的运行机制从而包含RxJava。感觉翻译的不通顺,附上原文:So you can change Retrofit’s execution mechanism to include RxJava by including the RxJava CallAdapter。

Step1

添加依赖

compile 'io.reactivex:rxjava:1.1.6'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'

Step2

创建Retrofit实例时添加一个CallAdapter——RxJavaCallAdapterFactory.create()

public static Retrofit getClient(String baseUrl) {
    if (retrofit==null) {
        retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }
    return retrofit;
}

Step3

当执行请求的时候,在可观测的刘发出事件时匿名的订阅者就会进行响应(When making the requests, our anonymous subscriber responds to the observable’s stream which emits events,),在我们的项目中就是SOAnswersResponse。当订阅者接收到发出的事件时onNext方法就会被调用并将时间传递到我们的适配器中。

@Override
public void loadAnswers() {      

mService.getAnswers().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<SOAnswersResponse>() {
                @Override
                public void onCompleted() {

                }

                @Override
                public void onError(Throwable e) {

                }

                @Override
                public void onNext(SOAnswersResponse soAnswersResponse) {
                    mAdapter.updateAnswers(soAnswersResponse.getItems());
                }
            });
}

查看 Ashraff Hathibelagal Getting Started With ReactiveX on Android的文章来学习更多关于RxJava 和 RxAndroid的知识。

结论

在这个教程中,你学到了关于Retrofit的以下知识:为什么使用以及如何使用它。我还解释了如何为Retrofit集成RxJava。在我的下一篇文章中,我会像你展示如何完成POST, PUT, 和 DELETE请求,如何发送Form-Urlencoded数据,以及如何取消请求。

要学习更多关于Retrofit的知识,一定要参照官方文档。同时,可以查看我们其它的一些Android app 开发课程可教程。

Communication Within an Android App With EventBus

Practical Concurrency on Android With HaMeR

Android From Scratch: Using REST APIs

Get Started With an Android App Template in 60 Seconds

最后,附上demo地址:
https://github.com/lyw-coder/RetrofitDemo

附:讲解很详细的一篇文章:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1117

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Retrofit2是一个用于在Android应用程序中进行网络请求的库。它可以帮助开发者简化网络请求的过程,并提供了一种方便的方式来处理网络响应。在使用Retrofit2之前,你需要创建一个Retrofit实例。你可以使用Retrofit.Builder类来构建这个实例,并通过设置一些参数来配置它。例如,你可以设置基本的URL,添加转换器和调用适配器等。\[1\]\[2\]\[3\] 你可以根据自己的需求选择不同的配置方式。一旦你创建了Retrofit实例,你就可以使用它来定义你的API接口,并发送网络请求。Retrofit2提供了一些注解来帮助你定义请求的方式、URL和参数等。你可以使用这些注解来简化你的代码,并使其更易读。同时,Retrofit2还支持异步请求和RxJava等功能,使得处理网络请求更加方便和灵活。总的来说,Retrofit2是一个强大而简单易用的网络请求库,可以帮助你更高效地进行网络通信。 #### 引用[.reference_title] - *1* *2* [Retrofit2完全教程](https://blog.csdn.net/xiangjai/article/details/51452217)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Android Retrofit2 使用教程](https://blog.csdn.net/qq_17798399/article/details/95001814)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值