Retrofit 简介 wiki 文档
Markdown版本笔记 | 我的GitHub首页 | 我的博客 | 我的微信 | 我的邮箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目录
Retrofit
WiKi 使用指导
Call Adapters
Converters 转换器
Retrofit Tutorials 教程资源
官网使用教程
Introduction 基本使用介绍
API Declaration 方法声明
Retrofit Configuration 配置
Retrofit 源码解析
Retrofit是如何创建接口实例的
Retrofit对象是如何构建的
Call对象是如何构建的
Retrofit
Type-safe HTTP client for Android and Java by Square, Inc.
GitHub主页
WIKI
官网&简易教程
系列教程
JAR包
Retrofit requires at minimum Java 7 or Android 2.3.
Snapshots of the development version are available in Sonatype's snapshots repository.
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0' //Converters 转换器
ProGuard 混淆
-dontwarn okio.**
-dontwarn javax.annotation.**
Retrofit uses Okio under the hood(在后台,在底层), so you may want to look at its ProGuard rules as well.
Retrofit 是一个 RESTful(一种架构风格)的 HTTP 网络请求框架的封装
。注意这里并没有说它是网络请求框架,主要原因在于网络请求的工作并不是Retrofit 来完成的。Retrofit2.0 内置 OkHttp,Retrofit 得益于 OkHttp 的优势,较之于 Volley 是一种更加先进的网络框架。
Retrofit 专注于接口的封装,OkHttp 专注于网络请求的高效,二者分工协作
!
我们的应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数
、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作,在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,Retrofit 根据用户的需求对结果进行解析的过程。
【Retrofit 2.0的变化】
在Retrofit 2.0中,最大的改动莫过于减小库的体积。
- 首先,Retrofit 2.0去掉了对所有的HTTP客户端的兼容,而钟情于OkHttpClient一个,极大地减少了各种适配代码
- 其次,拆库,比如将对RxJava的支持设置为可选(需要额外引入库)
- 再者,将各个序列化、反序列化转换器支持设置为可选(需要额外引入库)
WiKi 使用指导
Call Adapters
Retrofit is pluggable allowing different execution mechanisms and their libraries to be used for performing the HTTP call. This allows API requests to seamlessly compose with any existing threading model and/or task framework in the rest of your app.
Retrofit是可插拔的,允许不同的执行机制及其库用于执行HTTP调用。这允许API请求与您应用程序其余部分中的,任何现有线程模型,和/或任务框架无缝组合。
These are called call adapters, and Retrofit includes a few first-party modules for popular frameworks:
这些被称为call适配器,而Retrofit为当前非常流行的框架提供了一些第一方模块(官方构件):
- RxJava Observable & Single -
com.squareup.retrofit2:adapter-rxjava
- RxJava2 Observable, Flowable, Single, Completable & Maybe -
com.squareup.retrofit2:adapter-rxjava2
- Guava ListenableFuture -
com.squareup.retrofit2:adapter-guava
- Java 8 CompletableFuture -
com.squareup.retrofit2:adapter-java8
Various third-party adapters have been created by the community for other libraries:
社区也已经为其他库创建了各种的第三方适配器
Converters 转换器
Retrofit is pluggable allowing different serialization formats and their libraries to be used for converting Java types to their HTTP representation and parsing HTTP entities back into Java types.
Retrofit 是可插拔的,允许不同的序列化格式及其库,用于将Java类型转换为其HTTP表示形式,并将HTTP实体解析为Java类型。
These are called converters, and Retrofit includes a few first-party modules for popular frameworks:
这些被称为转换器,而Retrofit为当前非常流行的框架提供了一些第一方模块(官方构件),:
- Gson -
com.squareup.retrofit2:converter-gson
- Jackson -
com.squareup.retrofit2:converter-jackson
- Moshi -
com.squareup.retrofit2:converter-moshi
- Protobuf -
com.squareup.retrofit2:converter-protobuf
- Wire -
com.squareup.retrofit2:converter-wire
- Simple Framework -
com.squareup.retrofit2:converter-simplexml
- Scalars -
com.squareup.retrofit2:converter-scalars
Two delegating converters are also provided:
另外,还提供了两个委托转换器:
- Guava's
Optional<T>
-com.squareup.retrofit2:converter-guava
- Java 8's
Optional<T>
-com.squareup.retrofit2:converter-java8
These differ from the normal converters in that they don't actually convert bytes to object. Instead, they delegate to a normal converter for that and then wrap the optionally-nullable resulting value into an Optional.
这些与正常转换器不同之处在于,它们实际上并不将字节转换为对象。 相反,它们委托给一个正常的转换器,然后将可选的可空值结果值包装为可选。
Various third-party converters have been created by the community for other libraries and serialization formats:
社区也已经为其他库和序列化格式创建了各种的第三方转换器:
- LoganSquare -
com.github.aurae.retrofit2:converter-logansquare
- FastJson -
org.ligboy.retrofit2:converter-fastjson
ororg.ligboy.retrofit2:converter-fastjson-android
Retrofit Tutorials 教程资源
There are various Retrofit tutorials spread around the web and everyone is looking for help on different sites like Retrofit’s GitHub issues or Stackoverflow. Actually, there’s a tutorial series with more than 47 Retrofit guides available at Future Studio. Those guys are totally into Retrofit and help the community grow, make progress on problems and be awesome!
有各种各样的Retrofit教程遍布网络,每个人都在不同的网站寻找帮助,如Retrofit的GitHub issues或Stackoverflow。 实际上,在Future Studio上有一个具有超过47个可用的Retrofit指南的系列教程。 这些家伙完全进入了Retrofit,并且帮助社区发展,在解决问题上取得进展(大有裨益),并且很棒!
查找有关以下主题的指南:
- Getting Started with Retrofit 入门
- Retrofit 2 Upgrade Guide from 1.x 升级指南
- Handle authentication(处理认证) with Retrofit (Basic, custom OAuth 2, Token based, Hawk)
- Upload & download files
- Improve your development and debugging flow to test your app against multiple environments- and many more! 改进您的开发和调试流程,以测试您的应用程序对多个环境的适应 - 以及更多!
- Error Handling
You’ll find a lot more guides on various topics, have a look and benefit from all the solutions.
您可以在各种主题中找到更多指南,看一看并从所有这些解决方案中获益。
Enjoy coding and make your app rock!
享受编码,并让你的应用程序开始摇滚吧!
官网使用教程
Introduction 基本使用介绍
Retrofit turns your HTTP API into a Java interface.
Retrofit可以将你的HTTP API转化为JAVA的接口
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
The Retrofit class generates an implementation of the GitHubService interface.
通过Retrofit的create方法,就能生成一个GitHubService接口的实现
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("***")
.build();
GitHubService service = retrofit.create(GitHubService.class);
Each Call from the created GitHubService can make a synchronous or asynchronous HTTP request to the remote webserver.
每一个"来自创建的GitHubService接口的"Call对象,都可以向远程Web服务器发出同步或异步的HTTP请求。
Call<List<Repo>> repos = service.listRepos("octocat");
Use annotations to describe the HTTP request:
- URL parameter replacement and query parameter support。
- Object conversion to request body (e.g., JSON, protocol buffers)。
- Multipart request body and file upload。
Retrofit使用注解来描述HTTP请求:
- URL参数的替换和query参数的支持
- 对象转化为请求体(如:JSON,protocol buffers等)
- 多重请求体和文件上传
API Declaration 方法声明
Annotations on the interface methods and its parameters indicate how a request will be handled.
接口方法上的注解及其参数,表明一个请求需要怎么样被处理。
【REQUEST METHOD 请求方法】
Every method must have an HTTP annotation that provides the request method and relative URL. There are five built-in annotations: GET, POST, PUT, DELETE, and HEAD. The relative URL of the resource is specified in the annotation.
每一个方法必须要有一个HTTP注解,来标明请求的方式和相对URL。有五种内置的注解:GET、POST、PUT、DELETE以及HEAD。资源的相对URL需要在注解里面指定:
@GET("users/list")
You can also specify query parameters in the URL.
你也可以将query参数直接写在URL里:
@GET("users/list?sort=desc")
【URL MANIPULATION 操作】
A request URL can be updated dynamically using replacement blocks and parameters on the method. A replacement block is an alphanumeric string surrounded by { and }. A corresponding parameter must be annotated with @Path using the same string.
一个请求的URL可以通过"替换块和方法的参数"来进行动态的更新。替换块是由被
{}
包裹起来的"字母数字字符串"。相应的参数必须使用@Path
来注解同样的字符串。
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);
Query parameters can also be added.
Query参数也能同时被添加。
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
For complex query parameter combinations a Map can be used.
对于复杂的query参数,可以用Map来构建
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
【REQUEST BODY 请求体】
An object can be specified for use as an HTTP request body with the @Body annotation.
可以通过
@Body
注解来指定一个对象作为HTTP请求主体
@POST("users/new")
Call<User> createUser(@Body User user);
The object will also be converted using a converter specified on the Retrofit instance. If no converter is added, only RequestBody can be used.
这个对象会被Retrofit实例中指定的converter进行转化。如果没有给Retrofit实例添加任何converter,则只能使用RequestBody。
【FORM ENCODED AND MULTIPART 表单编码和MULTIPART】
Methods can also be declared to send form-encoded and multipart data.
方法也可以通过声明来发送form-encoded和multipart类型的数据。
Form-encoded data is sent when @FormUrlEncoded is present on the method. Each key-value pair is annotated with @Field containing the name and the object providing the value.
当方法中存在
@FormUrlEncoded
注解时,会发送表单编码数据。每个键值对都(需要)用@Filed
来注解,其中包含名称和提供该值对象。
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
Multipart requests are used when @Multipart is present on the method. Parts are declared using the @Part annotation.
当方法中存在
@Multipart
注解时,将使用Mutipart请求。Parts需要使用@Part
注解来声明。
@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);
Multipart parts use one of Retrofit's converters or they can implement RequestBody to handle their own serialization.
多个部件使用Retrofit其中的一个转换器,或者他们可以实现RequestBody来处理自己的序列化。
【HEADER MANIPULATION 头部操作】
You can set static headers for a method using the @Headers annotation.
你可以通过使用
@Headers
注解来为一个方法设置静态头。
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
多个静态头
@Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-Sample-App" })
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);
Note that headers do not overwrite each other. All headers with the same name will be included in the request.
请注意,头部参数并不会相互覆盖。具有同一个名称的所有头参数都会被包含进请求里面(即:允许key重复)。
A request Header can be updated dynamically using the @Header annotation. A corresponding parameter must be provided to the @Header. If the value is null, the header will be omitted. Otherwise, toString will be called on the value, and the result used.
可以使用
@Header
注解动态更新请求头。 相应的参数必须提供给@Header
注解。 如果这个值为null,那么这个头部参数就会被忽略。 否则,值的 toString 方法将会被调用,并且使用此结果。
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)
Headers that need to be added to every request can be specified using an OkHttp interceptor.
可以使用OkHttp拦截器来指定需要添加到每个请求中的头部参数。
【SYNCHRONOUS VS. ASYNCHRONOUS 同步 VS 异步】
Call instances can be executed either synchronously or asynchronously. Each instance can only be used once, but calling clone()
will create a new instance that can be used.
你可以同步或者异步地执行Call实例。每个实例只能被使用一次,但是调用 clone() 后将会创建一个可以使用的新的实例。
On Android, callbacks will be executed on the main thread. On the JVM, callbacks will happen on the same thread that executed the HTTP request.
在Android中,callback将会在主线程中执行;而在JVM环境中,callback将发生在和执行Http请求相同的那个线程中。
Retrofit Configuration 配置
Retrofit is the class through which your API interfaces are turned into callable objects. By default, Retrofit will give you sane defaults for your platform but it allows for customization.
Retrofit是将你定义的API接口转换为可调用对象的类。 默认情况下,Retrofit会提供给你"对您的平台来说"比较理智的默认值,但它允许自定义。
【CONVERTERS 转换器】
By default, Retrofit can only deserialize HTTP bodies into OkHttp's ResponseBody type and it can only accept its RequestBody type for @Body.
默认情况下,Retrofit只能将HTTP消息体反序列化为OKHttp的 ResonseBody 类型,而且只能接收 RequestBody 类型作为
@Body
。
Converters can be added to support other types. Six sibling modules adapt popular serialization libraries for your convenience.
可以添加转换器来支持其他类型。 以下六个同级模块,采用了常用的序列化库,来为你提供方便。
- Gson:
com.squareup.retrofit2:converter-gson
- Jackson:
com.squareup.retrofit2:converter-jackson
- Moshi:
com.squareup.retrofit2:converter-moshi
- Protobuf:
com.squareup.retrofit2:converter-protobuf
- Wire:
com.squareup.retrofit2:converter-wire
- Simple XML:
com.squareup.retrofit2:converter-simplexml
Here's an example of using the GsonConverterFactory class to generate an implementation of the GitHubService interface which uses Gson for its deserialization.
下面提供一个使用GsonConverterFactory类生成 GitHubService 的接口实现(通过使用Gson反序列化)的例子。
以下是使用GsonConverterFactory类生成使用Gson进行反序列化的GitHubService接口的实现的示例。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())//GsonConverterFactory
.build();
GitHubService service = retrofit.create(GitHubService.class);
【CUSTOM CONVERTERS 自定义转化器】
If you need to communicate with an API that uses a content-format that Retrofit does not support out of the box (e.g. YAML, txt, custom format) or you wish to use a different library to implement an existing format, you can easily create your own converter. Create a class that extends the Converter.Factory
class and pass in an instance when building your adapter.
如果你需要与 没有使用Retrofit提供的内容格式的API 进行交互(例如YAML、txt、或自定义格式),或者是你希望使用一个不同的库 来实现现有的格式,你可以轻松创建你自己的转化器。你需要创建一个继承自Converter.Factory的类,并且在构建适配器的时候传递一个实例。
Retrofit 源码解析
Retrofit是如何创建接口实例的
使用Retrofit时,我们需要先去定义一个接口,然后可以通过调用retrofit.create(IUserBiz.class)
方法,得到一个接口的实例,最后通过该实例执行我们的操作,那么Retrofit如何实现我们指定接口的实例呢?
其实原理是:动态代理
。
先看一个例子:
public class Test {
public static void main(String[] args) {
MInterface mInterface = (MInterface) Proxy.newProxyInstance(MInterface.class.getClassLoader(),//
new Class<?>[] { MInterface.class }, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法名:" + method.getName());//mMethod
System.out.println("参数列表:" + Arrays.toString(method.getParameters()));//[int arg0, java.lang.String arg1]
System.out.println("参数值:" + (Integer) args[0] + " , " + (String) args[1]);//28 , 包青天
GET get = method.getAnnotation(GET.class);
System.out.println("注解内容:" + get.value());//我是注解内容
return null;
}
});
mInterface.mMethod(28, "包青天");
}
}
interface MInterface {
@GET("我是注解内容")
void mMethod(int i, String s);
}
@Retention(RetentionPolicy.RUNTIME)
@interface GET {
String value();
}
可以看到我们通过Proxy.newProxyInstance
产生的代理类,当调用接口的任何方法时,都会调用InvocationHandler#invoke
方法,在这个方法中可以拿到传入的参数,注解等。所以Retrofit也可以通过同样的方式,在invoke方法里面,拿到所有的参数、注解信息,然后就可以去构造RequestBody,再去构建Request,得到Call对象封装后返回。
下面看retrofit#create
的源码结构:
public <T> T create(final Class<T> service) {
//***
return Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
//***
}
});
}
到这里,你应该明白Retrofit为我们接口生成实例对象并不神奇,仅仅是使用了Proxy这个类的API而已,然后在invoke方法里面拿到足够的信息去构建最终返回的Call而已。
Retrofit对象是如何构建的
这里依然是通过构造者模式
进行构建Retrofit对象,其内部的成员变量是比较少的,我们直接看build()
方法:
public Retrofit build() {
if(this.baseUrl == null) throw new IllegalStateException("Base URL required.");//baseUrl必须指定
else {
//如果你需要对okhttpclient进行详细的设置,需要构建OkHttpClient对象,然后通过callFactory方法传入,否则new一个默认的OkHttpClient
okhttp3.Call.Factory callFactory = this.callFactory;
if(callFactory == null) callFactory = new OkHttpClient();//OkHttpClient即为Factory的一个实现类
//Executor用来将回调传递到UI线程,这里可能会利用platform对象对平台进行判断,然后根据不同的平台将回调传递到不同的线程
Executor callbackExecutor = this.callbackExecutor;
if(callbackExecutor == null) callbackExecutor = this.platform.defaultCallbackExecutor();
//对Call进行转化,如转换为Observable后可使用RxJava
ArrayList adapterFactories = new ArrayList(this.adapterFactories);
adapterFactories.add(this.platform.defaultCallAdapterFactory(callbackExecutor));
//用于将 请求体/响应体 转换为我们想要的类型,比如转换后可通过Gson序列号/反序列化
ArrayList converterFactories = new ArrayList(this.converterFactories);
return new Retrofit((Factory)callFactory, this.baseUrl, converterFactories, adapterFactories, callbackExecutor, this.validateEagerly);
}
}
Call对象是如何构建的
我们构造完成retrofit,就可以利用retrofit.create
方法去构建接口的实例了,上面我们已经分析了这个环节利用了动态代理,而且我们也分析了具体的Call的构建流程在invoke方法中,下面看一下retrofit.create方法的完整代码:
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if(this.validateEagerly) this.eagerlyValidateMethods(service);
return Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, new InvocationHandler() {
private final Platform platform = Platform.get();
public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
if(method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
} else if(this.platform.isDefaultMethod(method)) {
return this.platform.invokeDefaultMethod(method, service, proxy, args);
} else {
//根据我们的method将其包装成ServiceMethod,ServiceMethod主要是通过解析我们方法上的注解最终构建一个Request对象
ServiceMethod serviceMethod = Retrofit.this.loadServiceMethod(method);
//通过ServiceMethod和方法的参数构造OkHttpCall对象,构造函数仅仅是简单的赋值
OkHttpCall okHttpCall = new OkHttpCall(serviceMethod, args);
//将OkHttpCall进行代理包装,类似装饰者模式,只不过将其执行时的回调通过callbackExecutor进行回调到UI线程中去了
return serviceMethod.callAdapter.adapt(okHttpCall);//ExecutorCallbackCall类型的Call对象
}
}
});
}
2017-9-13
Retrofit 简介 wiki 文档的更多相关文章
- Solr Wiki文档
相比ElasticSearch,Solr的文档详尽丰富,同时也显得冗余啰嗦. Solr的官方文档有两个地方: Solr官方教程 Solr社区维基 本文主要列出一些Solr Wiki中的主要讨论主题,方 ...
- 如何安装使用MinDoc搭建个人在线wiki文档
MinDoc是什么? MinDoc是一个在线的文档管理系统,该系统适用于团队.个人等使用.开发者最初的目的是为了便于公司内部使用,仿照看云开发.有laravel版本以及golang版本.不过larav ...
- rocksdb wiki文档阅读笔记
由于是英文文档,不做笔记过一阵就忘了,现在把关键点记录到这,开发的时候使用. 具体wiki地址:https://github.com/facebook/rocksdb/wiki 1)Column Fa ...
- wiki文档书写格式
文档基本规范 标题 标题:标明需求的简短语句.或模块名称,目录是由标题生成,一份目录结构清晰的需求文档与标题的划分是密不可分. 正文 正文:有规范格式和生效标志的正式文本,正文包括 文字.表格.图片. ...
- 26 JavaScript HTML DOM简介&方法&文档
HTML DOM: Document Object Model 文档对象模型.是HTML的标准对象模型和编程接口.(JavaScript只是可以操作HTML DOM的语言之一) 定义了HTML元素 ...
- Flask-Babel 使用简介(翻译文档)
最近用flask-bable翻译一个项目,在网站上查找到有一个示例文档,地址:http://translations.readthedocs.io/en/latest/flask-babel.html ...
- Wiki版产品需求---产品需求文档到底是谁的?产品到底是谁的?
在听了测试的一通唠叨之后,"内部实现一堆逻辑,只有一句话的需求文档","文档那么简单,我们怎么测试啊",心中突然想起来自己曾经干的一件当时觉得还不错的事情,但是 ...
- 使用Git Wiki 管理文档时,文档编写的基本用法
自己初次接触GitLab,通过百度和自己查找资料,了解了一部分.在自己的工作中,主要用到GitLab的Wiki文档版本管理能力.我总结了一小部分文本编辑需要用到的东西. 一.文本的排版 为了让文本/文 ...
- (翻译)W3C的Turtle文档
主要翻译如下页面,https://www.w3.org/TR/turtle/,对该页面中Turtle的内容部分进行翻译,希望对使用Turtle的朋友们有所帮助. 1 简介 2 Turtle语言 2.1 ...
随机推荐
- Android中selector背景选择器
http://blog.csdn.net/forsta/article/details/26148403 http://blog.csdn.net/wswqiang/article/details/6 ...
- jupyter notebook变量高亮
首先声明,anaconda安装的时候,一定要勾选“Add Anaconda to my PATH environment variable”! 否则会有一堆麻烦的问题,做了这一步就能自动添加好路径!不 ...
- mysql正则表达式,实现多个字段匹配多个like模糊查询
现在有这么一个需求 一个questions表,字段有题目(TestSubject),选项(AnswerA,AnswerB,AnswerC,AnswerD,AnswerE) 要求字段不包含png,jpg ...
- ubuntu 16.04.1 LTS postgresql安装配置
postgresql安装--------------------二进制安装:wget https://get.enterprisedb.com/postgresql/postgresql-9.5.6- ...
- PHP中var_export和var_dump的区别
var_dump -- 此函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值. var_export -- 输出或返回一个变量的字符串表示, 它和 var_dump() 类似,不同的是其返回 ...
- navicat premium 的使用——navicat 连接MySQL数据库
最近,在学习navicat premium这款第三方软件,他是一个三方数据库管理软件,在前一段时间实习的时候,我使用这款软件调用过服务器上的数据库,不过,当时并没有仔细研究,再次遇到这个软件,我决定仔 ...
- [BZOJ5298][CQOI2018]交错序列(DP+矩阵乘法)
https://blog.csdn.net/dream_maker_yk/article/details/80377490 斯特林数有时并没有用. #include<cstdio> #in ...
- 【拉格朗日插值法】【找规律】【高精度】Gym - 101156G - Non-Attacking Queens
题意:问你n*n的国际象棋棋盘上放3个互不攻击皇后的方案数. oeis……公式见代码内 //a(n) = 5a(n - 1) - 8a(n - 2) + 14a(n - 4) - 14a(n - 5) ...
- python开发_tkinter_复选菜单
在之前的blog中有提到python的tkinter中的菜单操作 python开发_tkinter_窗口控件_自己制作的Python IDEL_博主推荐 python开发_tkinter_窗口控件_自 ...
- IO流-复制多极文件夹(递归实现)
package com.io.test; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import ...