**Добавление зависимостей** *Способ 1:* Через library сгенерировать har-пакет, добавить har-пакет в папку libs. В файле entry Gradle добавить следующий код: implementation fileTree(dir: 'libs', include: ['*.jar', '*.har']) *Способ 2:* Для всех проектов: repositories { mavenCentral() } Если используется HttpURLConnection в качестве сетевого слоя: implementation 'io.openharmony.tpc.thirdlib:noHttpLibrary:1.0.1' Если нужно использовать OkHttp в качестве сетевого слоя, то дополнительно: implementation 'io.openharmony.tpc.thirdlib:noHttp-okhttpLibrary:1.0.1 **Общая инициализация** После прямой инициализации всё работает по умолчанию. NoHttp.initialize(this); **Расширенная инициализация** InitializationConfig config = InitializationConfig.newBuilder(context) // Другие настройки. ... .build(); NoHttp.initialize(config); Что касается таймаутов, многие люди не до конца понимают или неправильно понимают, я написал ответ на Zhihu, пожалуйста, обратитесь к нему: [HTTP 在什么情况下会请求超时?](https://www.zhihu.com/question/21609463/answer/160100810) Ниже приведены подробности о других настройках, которые были опущены выше. InitializationConfig config = InitializationConfig.newBuilder(context) // Глобальное время ожидания подключения к серверу, в миллисекундах, по умолчанию 10 секунд. connectionTimeout(30 * 1000) // Общее время ожидания ответа сервера, в миллисекундах, по умолчанию 10 секунд. readTimeout(30 * 1000) // Настройка кэша, по умолчанию используется база данных DBCacheStore, сохранение на SD-карту использует DiskCacheStore. cacheStore( // Если кэш не используется, setEnable(false) отключает его. new DBCacheStore(context).setEnable(true) ) // Конфигурация Cookie, по умолчанию сохраняется в базе данных DBCookieStore, разработчик может реализовать свой собственный CookieStore интерфейс. cookieStore( // Если cookie не поддерживается, setEnable(false) отключит его. new DBCookieStore(context).setEnable(true) ) // Сетевой уровень, по умолчанию URLConnectionNetworkExecutor, если вы хотите использовать OkHttp: OkHttpNetworkExecutor. networkExecutor() // Общий заголовок, add — это добавление, многократный вызов add не перезапишет предыдущий. addHeader() // Общий параметр, add — это добавление, многократный вызов add не перезапишет предыдущий. addParam() sslSocketFactory() // Общий SSLSocketFactory. hostnameVerifier() // Общий HostnameVerifier. retry(x) // Общее количество попыток, после настройки каждый неудачный запрос будет повторяться x раз. .build(); **Примечание:** 1. Все эти настройки можно настроить одновременно, а также можно настроить только некоторые из них. 2. addHeader() и addParam() можно вызывать несколько раз, и значения не будут перезаписаны. 3. При использовании DiskCacheStore() по умолчанию кэш сохраняется в context.getCacheDir(), при использовании DiskCacheStore(path) кэш сохраняется в каталоге path, но обратите внимание на разрешения на чтение и запись SD-карты и разрешения во время выполнения: AndPermission. Пример конфигурации кэша на SD-карте: InitializationConfig config = InitializationConfig.newBuilder(context) .cacheStore( new DiskCacheStore(context) // Сохраняется в папке context.getCahceDir(). // new DiskCacheStore(путь) // Сохраняется в пути, путь — это абсолютный путь, указанный разработчиком. ) .build(); Примеры добавления глобальных заголовков запросов и параметров: InitializationConfig config = InitializationConfig.newBuilder(context) .addHeader("Token", "123") // Глобальный заголовок запроса. .addHeader("Token", "456") // Глобальный заголовок запроса, не заменит предыдущий. .addParam("AppVersion", "1.0.0") // Глобальные параметры запроса. .addParam("AppType", "ohos") // Глобальные параметры запроса. .addParam("AppType", "iOS") // Глобальные параметры запроса, не заменят предыдущие два. .build(); **Необходимые разрешения** <..READ_EXTERNAL_STORAGE" /> <..WRITE_EXTERNAL_STORAGE" /> <..INTERNET" /> <..ACCESS_NETWORK_STATE" /> <...ACCESS_WIFI_STATE" /> **Режим отладки** Logger.setDebug(true);// Включает режим отладки NoHttp, после включения можно увидеть процесс запроса, журналы и сообщения об ошибках. Logger.setTag("NoHttpSample");// Устанавливает тег для печати Log. После включения режима отладки NoHttp можно увидеть процесс запроса, логи и сообщения об ошибках, и Log очень хорошо организован. Поэтому, если у разработчика возникнут какие-либо проблемы во время разработки, включите режим отладки, и все проблемы станут очевидными. **Сторонние асинхронные фреймворки** Ядро NoHttp — это синхронный метод запроса, асинхронные методы NoHttp (AsyncRequestExecutor, RequestQueue) основаны на синхронных запросах, поэтому RxJava, AsyncTask и другие могут хорошо инкапсулировать NoHttp. Пример запроса строки: StringRequest request = new String(url, RequestMethod.GET); Response<String> response = SyncRequestExecutor.INSTANCE.execute(request); if (response.isSucceed()) { // Запрос выполнен успешно. } else { // Ошибка запроса, получение ошибки: Exception e = response.getException(); } Вот два проекта из группы друзей, основанных на RxJava + NoHttp: 1. IRequest (автор: Юань Шэньбинь). 2. NohttpRxUtils (автор: Ли Ци). Разработчики могут использовать их в качестве справочного материала или напрямую использовать. # Синхронный запрос и асинхронный запрос Ядром модуля запроса NoHttp является синхронный запрос: SyncRequestExecutor; асинхронный запрос NoHttp делится на два типа: один — асинхронный исполнитель запроса: AsyncRequestExecutor, другой — очередь запросов: RequestQueue. **Синхронный запрос** Пример запроса строки: StringRequest req = new String("http://api.nohttp.net", RequestMethod.POST); Response<String> response = SyncRequestExecutor.INSTANCE.execute(req); if (response.isSucceed()) { // Запрос выполнен успешно. } else { // Ошибка запроса, получение ошибки: Exception e = response.getException(); } Конечно, синхронный запрос подходит только для использования в **дочернем потоке**, потому что основной поток ohos не разрешает инициировать сетевые запросы. Конечно, если использовать RxJava, AsyncTask и т. д., чтобы инкапсулировать синхронные запросы, их также можно использовать в основном потоке, но NoHttp предоставляет два способа асинхронного запроса, которые можно использовать непосредственно в основном потоке. **异步请求-AsyncRequestExecutor** ```StringRequest request = new StringRequest("http://api.nohttp.net"); Cancelable cancel = AsyncRequestExecutor.INSTANCE.execute(0, request, new SimpleResponseListener<String>() { @Override public void onSucceed(int what, Response<String> response) { // 请求成功。 } @Override public void onFailed(int what, Response<String> response) { // 请求失败。 }}); // 如果想取消请求: cancel.cancel(); // 判断是否取消: boolean isCancelled = cancel.isCancelled();``` Этот способ основан на использовании пула потоков и не имеет особенностей очереди с приоритетом. **异步 запрос — RequestQueue** ```RequestQueue queue = NoHttp.newRequestQueue(); // 默认 три параллельных потока, здесь можно передать количество параллельных потоков. ... // Отправляем запрос: queue.add(what, request, listener); ... // После использования необходимо закрыть очередь, чтобы освободить процессор: queue.stop();``` Также можно создать собственную очередь: ```// Также можно создать свою очередь: RequestQueue queue = new RequestQueue(5); queue.start(); // Запускаем очередь. ... // Отправляем запрос: queue.add(what, request, listener); ... // После использования необходимо остановить очередь: queue.stop();``` Многие разработчики имеют привычку создавать новую очередь для каждого отправленного запроса, что является **неправильным использованием**. Например, один из разработчиков создал метод: ```public <T> void request(Request<T> request, SampleResponseListener<T> listener) { RequestQueue queue = NoHttp.newRequestQueue(5); queue.add(0, request, listener);}``` Ещё раз подчёркиваю, что **этот подход является неправильным**. Для разработчиков, которые хотят вызывать очередь напрямую для отправки запросов, `NoHttp` также предоставляет одноэлементный режим: ```// Например, одноэлементный режим очереди запросов: NoHttp.getRequestQueueInstance().add... ... // Например, одноэлементный режим загрузки: NoHttp.getDownloadQueueInstance().add...``` Конечно, разработчики могут напрямую использовать асинхронный исполнитель запросов `AsyncRequestExecutor`, который рекомендуется к использованию. ### Правильное использование очереди Есть два правильных способа использования очереди: один — использовать отдельную очередь для каждой страницы и останавливать очередь при выходе со страницы; другой — использовать одну общую очередь и останавливать её при выходе из приложения. Я рекомендую второй способ, то есть использовать общую очередь `RequestQueue`. Способ 1: разработчик может создать базовый класс `BaseActivity`, в котором будет создана очередь в методе `onCreate()` и остановлена в методе `onDestory()`: ```public class BaseActivity extends Activity { private RequestQueue queue; @Override public void onCreate(Bundle savedInstanceState) { queue = NoHttp.newRequestQueue(); } // Предоставить дочерним классам возможность использовать запросы. public <T> void request(int what, Request<T> request, SimpleResponseListener<T> listener) { queue.add(what, request, listener); } @Override public void onDestory() { queue.stop(); }``` Способ 2: использовать одноэлементный шаблон для создания класса, который будет отвечать за все запросы в приложении, и поддерживать только одну очередь `RequestQueue`: ```StringRequest request = new StringRequest("http://api.nohttp.net", RequestMethod.POST); CallServer.getInstance().request(0, request, listener);``` Класс `CallServer` не предоставляется `NoHttp`, а должен быть создан разработчиком, так как здесь можно реализовать бизнес-логику приложения, поэтому разработчик может свободно экспериментировать: ```public class CallServer { private static CallServer instance; public static CallServer getInstance() { if (instance == null) synchronized (CallServer.class) { if (instance == null) instance = new CallServer(); } return instance; } private RequestQueue queue; private CallServer() { queue = NoHttp.newRequestQueue(5); } public <T> void request(int what, Request<T> request, SimpleResponseListener<T> listener) { queue.add(what, request, listener); } // Полностью выйти из приложения и вызвать этот метод, чтобы освободить ресурсы процессора. public void stop() { queue.stop(); }``` Обратите внимание, что в коде выше `listener` — это интерфейс обратного вызова, который принимает результаты запроса. На самом деле это `OnResponseListener`, и он должен реализовывать четыре метода. Иногда реализация всех четырёх методов может показаться сложной, поэтому `NoHttp` предоставляет реализацию по умолчанию `SimpleResponseListener`, где разработчик должен реализовать только необходимые методы. > В коде выше при добавлении запроса в очередь появляется параметр `what`, который аналогичен параметру `what` в `Handler` для сообщений. Он используется для определения того, какой запрос получил ответ. # Другие особенности и способы использования В следующем разделе будут представлены различные типы запросов, предоставляемые `NoHTTP`, такие как `String`, `Bitmap`, `JSONObject` и другие. Обычно разработчики используют `String` запросы, а затем преобразуют их в `JSON`, `XML` или `JavaBean`. Независимо от используемой сетевой библиотеки, это не лучший способ работы. Причины следующие: 1. Каждый запрос требует преобразования `String` в `XML`, `JSON` и т. д., что усложняет логику и приводит к избыточному коду. 2. Если объём данных большой, процесс преобразования может занять много времени, что ухудшит пользовательский опыт (приложение может зависнуть). Поэтому я написал статью о том, как объединить бизнес-логику и напрямую запрашивать `JavaBean`, `List`, `Map` и `Protobuf`: [http://blog.csdn.net/yanzhenjie1003/article/details/70158030](http://blog.csdn.net/yanzhenjie1003/article/details/70158030) ## Запрос различных типов данных Тип данных, запрашиваемых с помощью `NoHTTP`, определяется классом `Request`. `NoHTTP` уже предоставляет классы для запроса `String`, `Bitmap`, `JsonObject` и `JsonArray`: ``` // Запрос String: StringRequest request = new StringRequest(url, method); // Запрос Bitmap: ImageRequest request = new ImageRequest(url, method); // Запрос JSONObject: JsonObjectRequest request = new JsonObjectRequest(url, ``` **Метод:** // Запрос JSONArray: JsonArrayRequest request = new JsonArrayRequest(url, method); ``` **Сборка URL** Эта возможность была добавлена начиная с версии 1.1.3 и является одним из основных улучшений этого обновления — это метод сборки URL, например, если сервер использует RESTFUL API, то при запросе информации о пользователе URL может выглядеть следующим образом: ``` http://api.nohttp.net/rest/<userid>/userinfo ``` Здесь `<userid>` — это имя пользователя или идентификатор пользователя, который должен быть динамически заменён, а затем использован для получения информации о пользователе. Раньше это делалось так: ``` String userName = AppConfig.getUserName(); String url = "http://api.nohttp.net/rest/%1$s/userinfo"; url = String.format(Locale.getDefault(), url, userName); StringRequest request = new StringRequest(url); ... ``` Теперь это можно сделать так: ``` String url = "http://api.nohttp.net/rest/"; StringRequest request = new StringRequest(url) request.path(AppConfig.getUserName()) request.path("userinfo") ... ``` То есть разработчики теперь могут динамически собирать URL. **Добавление заголовков запроса** Заголовки запросов поддерживают добавление различных типов данных, таких как `String`, `int`, `long`, `double`, `float` и т. д. ``` StringRequest request = new StringRequest(url, RequestMethod.POST); .addHeader("name", "yanzhenjie") // String тип. .addHeader("age", "18") // int тип. .setHeader("sex", "男") // setHeader заменит уже существующий ключ. ... ``` **Добавление параметров** Заголовки запросов также поддерживают добавление различных типов данных, таких как `Binary`, `File`, `String`, `int`, `long`, `double`, `float` и т.д. ``` StringRequest request = new StringRequest(url, RequestMethod.POST); .add("name", "严振杰") // String тип .add("age", 18) // int тип .add("age", "20") // add метод не заменит уже существующий ключ, поэтому age будет иметь два значения: 18, 20. .set("sex", "女") // set заменит уже существующий ключ. .set("sex", "муж") // например, в итоге sex будет иметь только одно значение: муж. // Добавление File .add("head", file) .add("head", new FileBinary(file)) // Добавление Bitmap .add("head", new BitmapBinary(bitmap)) // Добавление ByteArray .add("head", new ByteArrayBinary(byte[])) // Добавление InputStream .add("head", new InputStreamBinary(inputStream)); ``` Кроме того, следует отметить, что старый метод `Request#add(Map<String, String>)` был обновлён до `Request#add(Map<String, Object>)`, что позволяет разработчикам, предпочитающим использовать `Map` для упаковки параметров, добавлять следующие типы параметров в `Map`: ``` String, File, Binary, List<String>, List<Binary>, List<File>, List<Object> ``` Пример кода: ``` Map<String, Object> params = new HashMap<>(); params.put("name", "yanzhenjie"); params.put("head", new File(path)); params.put("logo", new FileBinary(file)); params.put("age", 18); params.put("height", 180.5); List<String> hobbies = new ArrayList<>(); hobbies.add("篮球"); hobbies.add("帅哥"); params.put("hobbies", hobbies); List<File> goods = new ArrayList<>(); goods.add(file1); goods.add(file2); params.put("goods", goods); List<Object> otherParams = new ArrayList<>(); otherParams.add("yanzhenjie"); otherParams.add(1); otherParams.add(file); otherParams.add(new FileBinary(file)); params.put("other", otherParams); ``` Конечно, в реальной разработке использование одного и того же ключа для запроса файла вместе с другими данными практически невозможно, но вполне возможно, что `String` или `int` будут использоваться с одним и тем же ключом. Существует две формы загрузки файлов: первая — загрузка в виде формы, вторая — загрузка через тело запроса (`request body`). Рассмотрим первую форму загрузки в виде формы: * Один файл: ``` StringRequest request = ... request.add("file", new FileBinary(file)); ``` * Несколько файлов, загрузка разных файлов с разными ключами: Здесь можно добавить файлы различных форматов, такие как `File`, `Bitmap`, `InputStream`, `ByteArray`. ``` StringRequest request = ... request.add("file1", new FileBinary(File)); request.add("file2", new FileBinary(File)); request.add("file3", new InputStreamBinary(InputStream)); request.add("file4", new ByteArrayBinary(byte[])); request.add("file5", new BitmapBinary(Bitmap)); ``` * Загрузка нескольких файлов с одинаковыми ключами: ``` StringRequest request = ...; fileList.add("image", new FileBinary(File)); fileList.add("image", new InputStreamBinary(InputStream)); fileList.add("image", new ByteArrayBinary(byte[])); fileList.add("image", new BitmapStreamBinary(Bitmap)); ``` Или: ``` StringRequest request = ...; List<Binary> fileList = ...; fileList.add(new FileBinary(File)); fileList.add(new InputStreamBinary(InputStream)); fileList.add(new ByteArrayBinary(byte[])); fileList.add(new BitmapStreamBinary(Bitmap)); request.add("file_list", fileList); ``` Вторая форма загрузки через тело запроса (request body) очень разнообразна и позволяет отправлять не только файлы, но и любые потоки данных. Подробности смотрите в разделе «Отправка тела запроса». **Отправка тела запроса** Отправка тела запроса включает отправку `Json`, отправку `String`, отправку `Xml` и отправку потоков. На самом деле все они в конечном итоге преобразуются в поток и отправляются, поэтому разработчики могут использовать этот метод для отправки файлов. Конкретный способ использования следующий: ``` // Отправка обычной строки request.setDefineRequestBody(String, ContentType); // Отправка json-строки request.setDefineRequestBodyForJson(JsonString) // Отправка объекта JSONObject, который на самом деле является json-строкой request.setDefineRequestBodyForJson(JSONObject) // Отправка xml-строки request.setDefineRequestBodyForXML(XmlString) // Отправка тела шрифта, такого как файл (это отличается от загрузки формы), который можно преобразовать в InputStream и отправить ``` **request.setDefineRequestBody(InputStream, ContentType)** *Пример отправки файла:* ``` File file = ...; FileInputStream fileStream = new FileInputStream(file); StringRequest request = new StringRequest(url, RequestMethod.POST); request.setDefineRequestBody(fileStream, Headers.HEAD_VALUE_CONTENT_TYPE_OCTET_STREAM); ``` **Пять основных режимов кэширования** `NoHttp` поддерживает кэширование в базе данных, кэширование на SD-карту и т. д., и независимо от того, где находится кэш (в базе данных или на SD), `NoHttp` всегда шифрует данные, которые необходимо настроить при инициализации. Следует отметить, что если вы хотите кэшировать данные на SD-карте на телефоне с версией 6.0 и выше, вам необходимо запросить разрешение на выполнение перед запросом, если разработчик не понимает разрешения на выполнение, вы можете обратиться к этой статье [ohos 6.0 运行时权限管理最佳实践](http://blog.csdn.net/yanzhenjie1003/article/details/52503533). Автор рекомендует использовать этот фреймворк для управления разрешениями во время выполнения: [AndPermission](https://github.com/yanzhenjie/AndPermission). 1. **Режим по умолчанию** — это режим по умолчанию, когда не установлен режим кэширования. Этот режим реализует содержимое HTTP-протокола, например, ответный код равен 304, конечно, он также будет связан с E-Tag и LastModify и другими заголовками. ``` StringRequest request = new StringRequest(url, method); request.setCacheMode(CacheMode.DEFAULT); ``` 2. **Кэширование данных при неудачном запросе сервера** — запрос сервера успешен, возвращается серверные данные, в случае неудачи запроса сервера считываются данные из кэша и возвращаются. ``` StringRequest request = new StringRequest(url, method); request.setCacheMode(CacheMode.REQUEST_NETWORK_FAILED_READ_CACHE); ``` 3. **Если есть кэш, то сразу успешно, без кэша — запрос к серверу** — ядро ImageLoader помимо оптимизации памяти также обнаруживает наличие изображения внутри страны и использует его напрямую, а если нет, запрашивает сервер. Запрос `String`, кэширование `String`: ``` StringRequest request = new StringRequest(url, method); // 非标准Http协议,改变缓存模式为IF_NONE_CACHE_REQUEST_NETWORK request.setCacheMode(CacheMode.IF_NONE_CACHE_REQUEST_NETWORK); ``` Запрос изображения, кэширование изображения: ``` ImageRequest request = new ImageRequest(url, method); request.setCacheMode(CacheMode.IF_NONE_CACHE_REQUEST_NETWORK); ``` 4. **Только сетевой запрос** — независимо от обстоятельств будет только сетевой запрос, поддержка HTTP 304 и других действий по умолчанию отсутствует. ``` ImageRequest request = new ImageRequest(url, method); request.setCacheMode(CacheMode.ONLY_REQUEST_NETWORK); ... ``` 5. **Только чтение кэша** — независимо от ситуации будет считываться только кэш, сетевой запрос и другие операции отсутствуют. ``` Request<Bitmap> request = NoHttp.createImageRequest(imageUrl); request.setCacheMode(CacheMode.ONLY_READ_CACHE); ``` **Примечание**: если разработчик хочет сначала получить данные из кэша, а затем выполнить сетевой запрос, разработчик может сначала инициировать запрос только для чтения кэша, затем инициировать сетевой запрос только запроса, **но автор уже готовит NoHttp 2.0, и в будущем он будет представлен разработчикам в новом облике.** Режим кэширования поддерживает кэширование любых данных, поскольку `NoHttp` сохраняет данные в виде `byte[]`, а при чтении данных преобразует `byte[]` в данные, требуемые разработчиком, поэтому кэш `NoHttp` может поддерживать любые пользовательские запросы. **Пользовательский запрос** Все встроенные запросы `NoHttp` наследуются от класса `RestRequest`, поэтому пользовательский запрос также должен наследовать от `RestRequest`. Введите тип данных, который вы хотите запросить, в качестве параметра типа, и, наконец, проанализируйте данные сервера как тип данных, который вам нужен, в методе `parseResponse()`. *FastJsonRequest* ```java public class FastJsonRequest extends RestRequestor<JSONObject> { public FastJsonRequest(String url) { this(url, RequestMethod.GET); } public FastJsonRequest(String url, RequestMethod requestMethod) { super(url, requestMethod); } @Override public JSONObject parseResponse(Headers header, byte[] body) throws Throwable { String result = StringRequest.parseResponseString(headers, body); return JSON.parseObject(result); } } ``` Это всего лишь демонстрация пользовательского запроса. Например, разработчики могут также инкапсулировать бизнес-запросы в `Request` и напрямую запрашивать сложные данные, такие как JavaBean, List и т.д., связанные с бизнесом. Пожалуйста, обратитесь к этой статье: [http://blog.csdn.net/yanzhenjie1003/article/details/70158030](http://blog.csdn.net/yanzhenjie1003/article/details/70158030) **Загрузка файлов** Поскольку код загрузки файлов довольно длинный, здесь приводится ключевая часть, а конкретные детали можно найти в демонстрационном коде. Ядром модуля загрузки файлов `NoHttp` является синхронный запрос: `SyncDownloadExecutor`; асинхронная загрузка файлов `NoHttp` имеет только один способ очереди загрузки: `DownloadQueue`, конечно, вы также можете использовать `SyncDownloadExecutor` для объединения с `RxJava`, `AsyncTask` и другими способами для реализации асинхронной загрузки. **Синхронная загрузка — SyncDownloadExecutor** ``` DownloadRequest request = new DownloadRequest(url, RequestMethod.GET, fileFolder, true, true); SyncDownloadExecutor.INSTANCE.execute(0, request, new SimpleDownloadListener() { @Override public void onStart(int what, boolean resume, long range, Headers headers, long size) { // 开始下载,回调的时候说明文件开始下载了。 // 参数1:what。 // 参数2:是否是断点续传,从中间开始下载的。 // 参数3:如果是断点续传,这个参数非0,表示之前已经下载的文件大小。 // 参数4:服务器响应头。 // 参数5:文件总大小,可能为0,因为服务器可能不返回文件大小。 } @Override public void onProgress(int what, int progress, long fileCount, long speed) { // 进度发生变化,服务器不返回文件总大小时不回调,因为没法计算进度。 // 参数1:what。 // 继续... } @Override public void onFinish(int what, String filePath) { // 文件下载完成,回调。 // 参数1:what。 // 参数2:下载后的文件路径。 } }); ```