网络请求框架 Retrofit 2 使用入门
<p style="text-align:center"><img src="https://simg.open-open.com/show/38c07ac8ba4098daa6db5c369548e17c.jpg"></p> <p>你将要创造什么</p> <h2>Retrofit 是什么?</h2> <p>Retrofit 是一个用于 Android 和 Java 平台的类型安全的网络请求框架。Retrofit 通过将 API 抽象成 Java 接口而让我们连接到 REST web 服务变得很轻松。在这个教程里,我会向你介绍如何使用这个 Android 上最受欢迎和经常推荐的网络请求库之一。</p> <p>这个强大的库可以很简单的把返回的 JSON 或者 XML 数据解析成简单 Java 对象(POJO)。 GET , POST , PUT , PATCH , 和 DELETE 这些请求都可以执行。</p> <p>和大多数开源软件一样,Retrofit 也是建立在一些强大的库和工具基础上的。Retrofit 背后用了同一个开发团队的 <a href="/misc/goto?guid=4958860115824511695" rel="nofollow,noindex">OkHttp</a> 来处理网络请求。而且 Retrofit 不再内置 JSON 转换器来将 JSON 装换为 Java 对象。取而代之的是提供以下 JSON 转换器来处理:</p> <ul> <li>Gson: com.squareup.retrofit:converter-gson</li> <li>Jackson: com.squareup.retrofit:converter-jackson</li> <li>Moshi: com.squareup.retrofit:converter-moshi</li> </ul> <p>对于 Protocol Buffers , Retrofit 提供了:</p> <ul> <li> <p>Protobuf: com.squareup.retrofit2:converter-protobuf</p> </li> <li> <p>Wire: com.squareup.retrofit2:converter-wire</p> </li> </ul> <p>对于 XML 解析, Retrofit 提供了:</p> <ul> <li>Simple Framework: com.squareup.retrofit2:converter-simpleframework</li> </ul> <h2>那么我们为什么要用 Retrofit 呢?</h2> <p>开发一个自己的用于请求 REST API 的类型安全的网络请求库是一件很痛苦的事情:你需要处理很多功能,比如建立连接,处理缓存,重连接失败请求,线程,响应数据的解析,错误处理等等。从另一方面来说,Retrofit 是一个有优秀的计划,文档和测试并且经过考验的库,它会帮你节省你的宝贵时间以及不让你那么头痛。</p> <p>在这个教程里,我会构建一个简单的应用,根据 Stack Exchange API 查询上面最近的回答,从而来教你如何使用 Retrofit 2 来处理网络请求。我们会指明 /answers 这样一个路径,然后拼接到 base URL https://api.stackexchange.com/2.2 / 上执行一个 GET 请求——然后我们会得到响应结果并且显示到 RecyclerView 上。我还会向你展示如何利用 RxJava 来轻松地管理状态和数据流。</p> <h2>1.创建一个 Android Studio 工程</h2> <p>打开 Android Studio,创建一个新工程,然后创建一个命名为 MainActivity 的空白 Activity。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ac058d9dac7ad66b7ecbc27584f62856.png"></p> <h2>2. 添加依赖</h2> <p>创建一个新的工程后,在你的 build.gradle 文件里面添加以下依赖。这些依赖包括 RecyclerView,Retrofit 库,还有 Google 出品的将 JSON 装换为 POJO(简单 Java 对象)的 Gson 库,以及 Retrofit 的 Gson。</p> <pre> <code class="language-java">// 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'</code></pre> <p>不要忘记同步(sync)工程来下载这些库。</p> <h2>3. 添加网络权限</h2> <p>要执行网络操作,我们需要在应用的清单文件 <strong>AndroidManifest.xml</strong> 里面声明网络权限。</p> <pre> <code class="language-java"><?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></code></pre> <h2>4.自动生成 Java 对象</h2> <p>我们利用一个非常有用的工具来帮我们将返回的 JSON 数据自动生成 Java 对象: jsonschema2pojo 。</p> <h3>取得示例的 JSON 数据</h3> <p>复制粘贴 https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow 到你的浏览器地址栏,或者如果你熟悉的话,你可以使用 Postman 这个工具。然后点击 <strong>Enter</strong> —— 它将会根据那个地址执行一个 GET 请求,你会看到返回的是一个 JSON 对象数组,下面的截图是使用了 Postman 的 JSON 响应结果。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/05c60cf3acc83b2a32635583eceddacd.jpg"></p> <pre> <code class="language-java">{ "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 }</code></pre> <p>从你的浏览器或者 Postman 复制 JSON 响应结果。</p> <h3>将 JSON 数据映射到 Java 对象</h3> <p>现在访问 jsonschema2pojo ,然后粘贴 JSON 响应结果到输入框。</p> <p>选择 Source Type 为 <strong>JSON</strong> ,Annotation Style 为 <strong>Gson</strong> ,然后取消勾选 <strong>Allow additional properties</strong> 。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/82c6aa36f0a6523f6d3d5ad9779fb664.jpg"></p> <p>然后点击 <strong>Preview</strong> 按钮来生成 Java 对象。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/08a069078ba367dd63e1473272048a56.jpg"></p> <p>你可能想知道在生成的代码里面, @SerializedName 和 @Expose 是干什么的。别着急,我会一一解释的。</p> <p>Gson 使用 @SerializedName 注解来将 JSON 的 key 映射到我们类的变量。为了与 Java 对类成员属性的驼峰命名方法保持一致,不建议在变量中使用下划线将单词分开。 @SerializeName 就是两者的翻译官。</p> <pre> <code class="language-java">@SerializedName("quota_remaining") @Expose private Integer quotaRemaining;</code></pre> <p>在上面的示例中,我们告诉 Gson 我们的 JSON 的 key quota_remaining 应该被映射到 Java 变量 quotaRemaining 上。如果两个值是一样的,即如果我们的 JSON 的 key 和 Java 变量一样是 quotaRemaining ,那么就没有必要为变量设置 @SerializedName 注解,Gson 会自己搞定。</p> <p>@Expose 注解表明在 JSON 序列化或反序列化的时候,该成员应该暴露给 Gson。</p> <h3>将数据模型导入 Android Studio</h3> <p>现在让我们回到 Android Studio。新建一个 <strong>data</strong> 的子包,在 data 里面再新建一个 <strong>model</strong> 的包。在 model 包里面,新建一个 Owner 的 Java 类。</p> <p>然后将 jsonschema2pojo 生成的 Owner 类复制粘贴到刚才新建的 Owner 类文件里面。</p> <pre> <code class="language-java">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; } }</code></pre> <p>利用同样的方法从 jsonschema2pojo 复制过来,新建一个 Item 类。</p> <pre> <code class="language-java">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; } }</code></pre> <p>最后,为返回的 StackOverflow 回答新建一个 SOAnswersResponse 类。注意在 jsonschema2pojo 里面类名是 Example ,别忘记把类名改成 SOAnswersResponse 。</p> <pre> <code class="language-java">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; } }</code></pre> <h2>5. 创建 Retrofit 实例</h2> <p>为了使用 Retrofit 向 REST API 发送一个网络请求,我们需要用 Retrofit.Builder 类来创建一个实例,并且配置一个 base URL。</p> <p>在 data 包里面新建一个 remote 的包,然后在 remote 包里面新建一个 RetrofitClient 类。这个类会创建一个 Retrofit 的单例。Retrofit 需要一个 base URL 来创建实例。所以我们在调用 RetrofitClient.getClient(String baseUrl) 时会传入一个 URL 参数。参见 13 行,这个 URL 用于构建 Retrofit 的实例。参见 14 行,我们也需要指明一个我们需要的 JSON converter(Gson)。</p> <pre> <code class="language-java">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; } }</code></pre> <h2>6.创建 API 接口</h2> <p>在 remote 包里面,创建一个 SOService 接口,这个接口包含了我们将会用到用于执行网络请求的方法,比如 GET , POST , PUT , PATCH , 以及 DELETE 。在该教程里面,我们将执行一个 GET 请求。</p> <pre> <code class="language-java">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); }</code></pre> <p>GET 注解明确的定义了当该方法调用的时候会执行一个 GET 请求。接口里每一个方法都必须有一个 HTTP 注解,用于提供请求方法和相对的 URL 。Retrofit 内置了 5 种注解: @GET , @POST , @PUT , @DELETE , 和 @HEAD 。</p> <p>在第二个方法定义中,我们添加一个 query 参数用于从服务端过滤数据。Retrofit 提供了 @Query("key") 注解,这样就不用在地址里面直接写了。key 的值代表了 URL 里参数的名字。Retrofit 会把他们添加到 URL 里面。比如说,如果我们把 android 作为参数传递给 getAnswers(String tags) 方法,完整的 URL 将会是:</p> <pre> <code class="language-java">https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow&tagged=android</code></pre> <p>接口方法的参数有以下注解:</p> <table> <thead> <tr> <th> </th> <th> </th> <th> </th> </tr> </thead> <tbody> <tr> <td>@Path</td> <td>替换 API 地址中的变量</td> </tr> <tr> <td>@Query</td> <td>通过注解的名字指明 query 参数的名字</td> </tr> <tr> <td>@Body</td> <td>POST 请求的请求体</td> </tr> <tr> <td>@Header</td> <td>通过注解的参数值指明 header</td> </tr> </tbody> </table> <h2>7.创建 API 工具类</h2> <p>现在我们要新建一个工具类。我们命名为 ApiUtils 。该类设置了一个 base URL 常量,并且通过静态方法 getSOService() 为应用提供 SOService 接口。</p> <pre> <code class="language-java">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); } }</code></pre> <h2>8.显示到 RecyclerView</h2> <p>既然结果要显示到 RecyclerView 上面,我们需要一个 adpter。以下是 AnswersAdapter 类的代码片段。</p> <pre> <code class="language-java">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); } }</code></pre> <h2>9.执行请求</h2> <p>在 MainActivity 的 onCreate() 方法内部,我们初始化 SOService 的实例(参见第 9 行),RecyclerView 以及 adapter。最后我们调用 loadAnswers() 方法。</p> <pre> <code class="language-java">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(); }</code></pre> <p>loadAnswers() 方法通过调用 enqueue() 方法来进行网络请求。当响应结果返回的时候,Retrofit 会帮我们把 JSON 数据解析成一个包含 Java 对象的 list(这是通过 GsonConverter 实现的)。</p> <pre> <code class="language-java">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"); } }); }</code></pre> <h2>10. 理解 enqueue()</h2> <p>enqueue() 会发送一个异步请求,当响应结果返回的时候通过回调通知应用。因为是异步请求,所以 Retrofit 将在后台线程处理,这样就不会让 UI 主线程堵塞或者受到影响。</p> <p>要使用 enqueue() ,你必须实现这两个回调方法:</p> <ul> <li>onResponse()</li> <li>onFailure()</li> </ul> <p>只有在请求有响应结果的时候才会调用其中一个方法。</p> <ul> <li>onResponse() :接收到 HTTP 响应时调用。该方法会在响应结果能够被正确地处理的时候调用,即使服务器返回了一个错误信息。所以如果你收到了一个 404 或者 500 的状态码,这个方法还是会调用。为了拿到状态码以便后续的处理,你可以使用 response.code() 方法。你也可以使用 isSuccessful() 来确定返回的状态码是否在 200-300 范围内,该范围的状态码也表示响应成功。</li> <li>onFailure() :在与服务器通信的时候发生网络异常或者在处理请求或响应的时候发生异常的时候调用。</li> </ul> <p>要执行同步请求,你可以使用 execute() 方法。要注意同步请求在主线程会阻塞用户的任何操作。所以不要在主线程执行同步请求,要在后台线程执行。</p> <h2>11.测试应用</h2> <p>现在你可以运行应用了。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/38c07ac8ba4098daa6db5c369548e17c.jpg"></p> <h2>12. 结合 RxJava</h2> <p>如果你是 RxJava 的粉丝,你可以通过 RxJava 很简单的实现 Retrofit。RxJava 在 Retrofit 1 中是默认整合的,但是在 Retrofit 2 中需要额外添加依赖。Retrofit 附带了一个默认的 adapter 用于执行 Call 实例,所以你可以通过 RxJava 的 CallAdapter 来改变 Retrofit 的执行流程。</p> <h3>第一步</h3> <p>添加依赖。</p> <pre> <code class="language-java">compile 'io.reactivex:rxjava:1.1.6' compile 'io.reactivex:rxandroid:1.2.1' compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'</code></pre> <h3>第二步</h3> <p>在创建新的 Retrofit 实例的时候添加一个新的 CallAdapter RxJavaCallAdapterFactory.create() 。</p> <pre> <code class="language-java">public static Retrofit getClient(String baseUrl) { if (retrofit==null) { retrofit = new Retrofit.Builder() .baseUrl(baseUrl) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; }</code></pre> <h3>第三步</h3> <p>当我们执行请求时,我们的匿名 subscriber 会响应 observable 发射的事件流,在本例中,就是 SOAnswersResponse 。当 subscriber 收到任何发射事件的时候,就会调用 onNext() 方法,然后传递到我们的 adapter。</p> <pre> <code class="language-java">@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()); } }); }</code></pre> <p>查看 Ashraff Hathibelagal 的 Getting Started With ReactiveX on Android 以了解更多关于 RxJava 和 RxAndroid 的内容。</p> <h2>总结</h2> <p>在该教程里,你已经了解了使用 Retrofit 的理由以及方法。我也解释了如何将 RxJava 结合 Retrofit 使用。在我的下一篇文章中,我将为你展示如何执行 POST , PUT , 和 DELETE 请求,如何发送 Form-Urlencoded 数据,以及如何取消请求。</p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/d3fdf84ead4b</p> <p> </p>
本文由用户 RobertaBras 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!