Okio для Kotlin: Полное руководство по I/O библиотеке от Square
Okio — это современная библиотека ввода-вывода (I/O) для Kotlin и Java, разработанная компанией Square. Она создана как альтернатива стандартным потокам Java (java.io, java.nio) и предлагает более удобный, производительный и безопасный API для работы с данными. Okio решает ключевые проблемы стандартных I/O библиотек: громоздкий код, сложность работы с буферами, неэффективное копирование данных и отсутствие поддержки современных парадигм программирования.
Зачем нужна Okio? Во-первых, она предоставляет концепцию Source (источник) и Sink (приёмник) вместо InputStream/OutputStream. Это делает код более читаемым и предсказуемым. Во-вторых, библиотека использует собственные буферы (Buffer), которые работают быстрее стандартных за счёт оптимизации копирования и управления памятью. В-третьих, Okio идеально интегрируется с Kotlin: поддерживает корутины, extension-функции и null-safety. Библиотека является фундаментом для таких популярных проектов, как OkHttp (HTTP-клиент) и Retrofit (REST-клиент), что подтверждает её надёжность и производительность.
Okio особенно полезна при разработке сетевых приложений, работе с файловыми системами, сжатии данных, сериализации и любых сценариях, где требуется эффективная работа с потоками данных. Она позволяет писать меньше кода, избегать типичных ошибок (например, утечки ресурсов) и получать более высокую производительность по сравнению с традиционными подходами.
Установка
Okio распространяется через Maven Central. Для подключения к проекту на Kotlin (Gradle) добавьте зависимость в файл build.gradle.kts:
// build.gradle.kts
dependencies {
implementation("com.squareup.okio:okio:3.9.0")
}
Если вы используете Maven, добавьте в pom.xml:
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>3.9.0</version>
</dependency>
Для платформы Android убедитесь, что минимальный SDK — 21 или выше. Okio версии 3.x требует Kotlin 1.6+ и Java 8+.
Быстрый старт
Минимальный рабочий пример — чтение данных из файла и запись в другой файл:
import okio.*
import java.io.File
fun main() {
val sourceFile = File("input.txt")
val sinkFile = File("output.txt")
// Создаём Source и Sink
val source: Source = sourceFile.source()
val sink: Sink = sinkFile.sink()
// Буфер для временного хранения данных
val buffer = Buffer()
// Читаем из source в буфер, затем пишем из буфера в sink
source.readAll(buffer)
sink.write(buffer, buffer.size)
// Закрываем ресурсы
source.close()
sink.close()
println("Данные скопированы из ${sourceFile.name} в ${sinkFile.name}")
}
Этот код копирует содержимое одного файла в другой. Обратите внимание: мы используем source() и sink() — extension-функции для File, которые возвращают Source и Sink соответственно. Буфер Buffer выступает как временное хранилище, минимизируя количество системных вызовов.
Основные методы, функции и классы
Ниже представлены ключевые компоненты Okio. Для каждого приведена сигнатура, описание и пример использования.
1. Buffer — класс для работы с буферизованными данными
Сигнатура: class Buffer : BufferedSource, BufferedSink
Описание: Основной класс для хранения и манипуляции данными в памяти. Реализует как BufferedSource, так и BufferedSink, что позволяет одновременно читать и писать. Буфер организован как сегментированная структура, что ускоряет операции копирования.
import okio.Buffer
fun main() {
val buffer = Buffer()
// Записываем строку в буфер
buffer.writeUtf8("Hello, Okio!")
println("Размер буфера: ${buffer.size}") // 12
// Читаем строку из буфера
val content = buffer.readUtf8()
println("Содержимое: $content") // Hello, Okio!
// Буфер пуст после чтения
println("Буфер пуст: ${buffer.exhausted()}") // true
}
2. Source — интерфейс для чтения данных
Сигнатура: interface Source : Closeable
Описание: Базовый интерфейс для всех источников данных (файлы, сетевые потоки и т.д.). Содержит метод read(Buffer, Long) для чтения данных в буфер. В отличие от InputStream, Source не выбрасывает IOException на каждый чих, а использует более строгую систему исключений.
import okio.*
import java.io.File
fun main() {
val file = File("example.txt")
file.writeText("Kotlin & Okio")
val source: Source = file.source()
val buffer = Buffer()
// Читаем до 1024 байт в буфер
val bytesRead = source.read(buffer, 1024)
println("Прочитано байт: $bytesRead") // 14
val content = buffer.readUtf8()
println("Содержимое: $content") // Kotlin & Okio
source.close()
}
3. Sink — интерфейс для записи данных
Сигнатура: interface Sink : Closeable, Flushable
Описание: Базовый интерфейс для всех приёмников данных. Метод write(Buffer, Long) записывает данные из буфера. Sink гарантирует, что данные будут записаны атомарно (или будет выброшено исключение).
import okio.*
import java.io.File
fun main() {
val file = File("output.txt")
val sink: Sink = file.sink()
val buffer = Buffer()
buffer.writeUtf8("Запись через Okio!")
sink.write(buffer, buffer.size)
sink.close()
println("Файл создан: ${file.readText()}") // Запись через Okio!
}
4. BufferedSource — буферизованный источник
Сигнатура: interface BufferedSource : Source
Описание: Расширяет Source, добавляя методы для чтения строк, чисел, байтовых массивов и других типов данных с использованием внутреннего буфера. Это основной интерфейс для удобного чтения данных.
import okio.*
import java.io.File
fun main() {
val file = File("data.bin")
file.writeBytes(byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F)) // "Hello"
val source: BufferedSource = file.source().buffer()
// Читаем байт
val byte = source.readByte()
println("Первый байт: ${byte.toChar()}") // H
// Читаем строку до конца
val rest = source.readUtf8()
println("Остаток: $rest") // ello
source.close()
}
5. BufferedSink — буферизованный приёмник
Сигнатура: interface BufferedSink : Sink
Описание: Расширяет Sink, предоставляя удобные методы для записи строк, чисел, байтовых массивов и других типов. Буферизация уменьшает количество операций ввода-вывода.
import okio.*
import java.io.File
fun main() {
val file = File("output.bin")
val sink: BufferedSink = file.sink().buffer()
sink.writeByte(0x48) // 'H'
sink.writeUtf8("ello, Okio!")
sink.writeInt(2024) // 4 байта
sink.close()
println("Размер файла: ${file.length()}") // 17 (1 + 12 + 4)
}
6. Okio.source() и Okio.sink() — фабричные методы
Сигнатура: fun InputStream.source(): Source
fun OutputStream.sink(): Sink
Описание: Extension-функции, преобразующие стандартные Java потоки в Source/Sink. Позволяют интегрировать Okio с существующим кодом.
import okio.*
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
fun main() {
val inputData = "Данные из InputStream".toByteArray()
val inputStream = ByteArrayInputStream(inputData)
val outputStream = ByteArrayOutputStream()
// Преобразуем InputStream в Source
val source: Source = inputStream.source()
val sink: Sink = outputStream.sink()
val buffer = Buffer()
source.readAll(buffer)
sink.write(buffer, buffer.size)
source.close()
sink.close()
println("Результат: ${String(outputStream.toByteArray())}") // Данные из InputStream
}
7. Buffer.writeUtf8() и Buffer.readUtf8() — работа со строками
Сигнатура: fun writeUtf8(string: String): BufferedSink
fun readUtf8(): String
Описание: Методы для записи и чтения строк в кодировке UTF-8. Okio автоматически обрабатывает кодировку, что исключает ошибки, связанные с разными кодировками.
import okio.Buffer
fun main() {
val buffer = Buffer()
buffer.writeUtf8("Привет, мир!")
buffer.writeUtf8(" Hello, World!")
// Читаем всю строку
val fullString = buffer.readUtf8()
println(fullString) // Привет, мир! Hello, World!
// Альтернатива: читаем до определённого размера
buffer.writeUtf8("1234567890")
val part = buffer.readUtf8(5)
println(part) // 12345
}
8. Timeout — управление таймаутами
Сигнатура: class Timeout
fun timeout(): Timeout (метод Source/Sink)
Описание: Позволяет устанавливать таймауты на операции чтения/записи. Okio использует собственный механизм таймаутов, который работает корректно даже с прерыванием потоков.
import okio.*
import java.io.File
import kotlin.concurrent.thread
fun main() {
val file = File("slow.txt")
file.writeText("Данные")
val source: Source = file.source()
// Устанавливаем таймаут 100 мс
source.timeout().timeout(100, java.util.concurrent.TimeUnit.MILLISECONDS)
val buffer = Buffer()
try {
source.readAll(buffer)
println("Прочитано: ${buffer.readUtf8()}")
} catch (e: Exception) {
println("Таймаут или ошибка: ${e.message}")
} finally {
source.close()
}
}
9. GzipSink и GzipSource — сжатие данных
Сигнатура: class GzipSink(sink: Sink) : Sink
class GzipSource(source: Source) : Source
Описание: Декораторы, добавляющие сжатие/распаковку GZIP к любому Sink/Source. Позволяют легко работать со сжатыми данными без ручного управления буферами.
import okio.*
import java.io.File
fun main() {
val originalFile = File("data.txt")
originalFile.writeText("Повторяющиеся данные. Повторяющиеся данные. ")
// Сжатие
val gzipFile = File("data.gz")
val sink = GzipSink(gzipFile.sink())
val buffer = Buffer()
buffer.writeAll(originalFile.source())
sink.write(buffer, buffer.size)
sink.close()
println("Исходный размер: ${originalFile.length()}, сжатый: ${gzipFile.length()}")
// Распаковка
val source = GzipSource(gzipFile.source())
val decompressed = Buffer()
source.readAll(decompressed)
source.close()
println("Распаковано: ${decompressed.readUtf8()}")
}
10. HashingSource и HashingSink — вычисление хешей
Сигнатура: class HashingSource(source: Source, hashFunction: HashFunction)
class HashingSink(sink: Sink, hashFunction: HashFunction)
Описание: Декораторы, которые вычисляют хеш (MD5, SHA-1, SHA-256) данных "на лету" во время чтения или записи. Полезно для проверки целостности данных.
import okio.*
import java.io.File
fun main() {
val file = File("document.pdf")
if (!file.exists()) {
println("Файл не найден, создаём тестовый")
file.writeText("Тестовый документ для хеширования")
}
// Читаем файл и вычисляем SHA-256
val source = file.source()
val hashingSource = HashingSource(source, HashFunction.SHA256)
val buffer = Buffer()
hashingSource.readAll(buffer)
hashingSource.close()
val hash = hashingSource.hash()
println("SHA-256 хеш файла: $hash")
// Пример: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
}
Реальный практический пример
Разработаем утилиту для резервного копирования с шифрованием и сжа