در این پست قصد یادگیری مفهومی و مناسب از عملیات خواندن/نوشتن (Input/Output) را از رم به دیسک و بالعکس را داریم. احتمالا شما هم در هنگام انجام پروژهها با هرگونه سطح چالش با این مسئله مواجه شدهاید که نیاز بوده فایلی را در دیسک (مموری کارد) اندروید ذخیره کنید یا آن را وارد اپلیکیشن خود کنید.
لیست کلمات و معانی آن از نظر نویسنده:
دریافت سمپل کد برای پروژه:
نسخه انگلیسی مقاله:
منظور از I/O چیست؟
تمامی کلاسهایی که ما در این پست آموزش استفاده از آنها را یاد میگیریم در پکیج java.io قرار گرفتهاند.
دستگاه فیزیکی چیست؟
کیبورد، صفحه نمایش و دیسک همگی مثالهای دستگاههای فیزیکی میباشند.
در این پست هدف ما یادگیری ارتباط بین دیسک و رم میباشد.
منظور از Stream چیست؟
استریم (جریان داده) عبارت است از یک پل ارتباطی که بین دستگاه فیزیکی و رم برای کمک به تبادل اطلاعات صورت میگیرد.
جاوا از دو نوع استریم پشتیبانی میکند:
برای ذخیره/خواندن فایلهای نوشتاری (Text) از Character Stream و برای هر نوع فایل دیگر از جمله عکسها ، ویدیو و ... از Byte Stream استفاده میکنیم.
در نمودار بالا هر چهار کلاس نمایش داده شده (مستطیلها) از نوع Abstract میباشند.
مفهوم Abstract در Java و Kotlin مربوط به کلاسی میباشد که توانایی ایجاد نمونه (شی) از خود نداشته و حتما نیاز است که کلاسی از آن ارثبری (Extend) کند.
با توجه به مفهوم بالا نیاز داریم که از Subclass های آنها استفاده کنیم.
کلاس File برای کار با فایلها و پوشهها
کلاسی که به شما تنها اجازه دسترسی به اطلاعات فایلهای درون دیسک را میدهد، اطلاعاتی همچون محل ذخیره، نوع و حجم آن. میتوان این کلاس را مثل یک کاغذ کادویی به دور یک هدیه در نظر گرفت، کاغذ کادویی شامل شکل و اندازه، محل قرارگیری کادو میباشد ولی خود هدیه نیست.
val file = File(filesDir, "hello.txt") val fw = FileWriter(file) fw.write("Hello") fw.write("\n") fw.write("World!") fw.close()
دلیل بستن FileWriter چیست؟
در هنگام کار با کلاسهای I/O برای نهایی شدن عملیات خواندن/نوشتن و آزاد شدن منابع تحت تسلط آنها حتما باید آن را ببندید، در غیر این صورت عملیات نهایی نمیشود و در نتیجه فایل شما ساخته نمیشود.
در صورت نیاز به انجام عملیات بدون بستن استریم میتوانید از فانکشن flush به جای close استفاده کنید.
در صورتی که نحوه دسترسی به فایل ذخیره شده در دیسک را نمیدانید، میتوانید با استفاده از راهنمای انتهای مقاله آن را یاد بگیرید.
در اینجا یک مشکل سرعت انجام عملیات داریم! برای ذخیره سازی تک تک کاراکترهایهای نیاز است که به سیستمعامل دستور داده شود که آن را بر روی دیسک (مموری کارد) ذخیره کند.
مفهوم Buffer چیست؟
بافر قسمتی از رم میباشد که اطلاعات را قبل از خواندن/نوشتن در خود ذخیره میکند تا سرعت عملیات را افزایش دهد. وقتی بافر پر شد حالا تنها در یک قدم اطلاعات ارسال/دریافت میشوند و دیگر نیازی به فراخوانیهای مجدد سیستمعامل نیست. بدون استفاده از Buffer برای ذخیرهسازی 500 کاراکتر نیاز به 500 فراخوانی سیستمعامل وجود دارد.
کلمه Buffer در معنای لغوی به معنای میانگیر میباشد.
val file = File(filesDir, "hello.txt") val fw = FileWriter(file) val bw = BufferedWriter(fw) bw.write("Hello") bw.newLine() bw.write("World!") bw.close()
چرا FileWriter را به عنوان ورودی BufferedWriter دادیم؟
کلاسهای استریم پکیج java.io یا از نوع اتصالی (Connection) و یا از نوع زنجیری (Chain) میباشند.
با بستن بالاترین استریم باقی نیز به صورت خودکار بسته خواهند شد، پس با بستن BufferedWriter دیگر نیازی به بستن FileWriter به صورت جداگانه نداریم.
جاوا از الگوی طراحیای (Design Pattern) به نام Decorator برای اتصال این کلاسها به هم استفاده میکند. با ترکیب این کلاسها با یکدیگر میتوان کارایی کلاسهای استریم را تغییر داد.
val file = File(filesDir, "hello.txt") val fr = FileReader(file) val br = BufferedReader(fr) var line: String? = "" val sb = StringBuilder() while (br.readLine().also { line = it } != null) { sb.append(line + "\n") } bis.close()
توضیحاتی در مورد این کد
کلاس BufferReader به ما نوشتهها رو خط به خط میده در نتیجه در داخل یک حلقه تا زمانی که خطها به پایان نرسیدن اونها رو به StringBuilder اضافه میکنیم.
فانکشن .also هم یکی از Scope Function های کاتلین هستش که بعد از انجام کاری (خواندن خط در این حالت) کار دیگه ای رو هم انجام میده بدون اینکه تغییری روی مقدار قبلی بزاره.
خط رو بخون و همچنین (also) اون رو داخل متغییر line ذخیره کن بدون اینکه تاثیری روی مقدارش بزاری و حالا چک کن خطی که خوندی null نباشه.
در این مرحله با Byte Stream ها آشنا میشویم.
منظور از Serialization و Deserialization چیست؟
به عملیات تبدیل یک شی (Object) به فایل برای ذخیره وضعیت آن شی Serialization گفته میشود.
در صورتی که فایل را به حالت شی (Object) بودن آن برگردانیم این عمل Deserialization نامیده میشود.
در پلتفرم اندروید به جای Serialization استفاده از Parcelable برای افزایش سرعت این فرآیند توصیه میشود.
val file = File(filesDir, "hello.txt") val person = Person("Keanu Reeves", 55) val fos = FileOutputStream(file) val bos = BufferedOutputStream(fos) val oos = ObjectOutputStream(bos) oos.writeObject(person) oos.close() val fis = FileInputStream(file) val bis = BufferedInputStream(fis) val ois = ObjectInputStream(bis) val revivedPerson = ois.readObject() as Person ois.close()
دلیل استفاده از ObjectInputStream و ObjectOutputStream چیست؟
در مبحث نوع اتصال استریمها متوجه شدیم که استریمهای زنجیری (Chain Streams) برای افزودن عملکرد به استریمهای اتصالی (Connection Stream) وصل میشوند. در اینجا هم از ObjectOutputStream برای تبدیل شی (Object) مورد نظر به بایتها، برای ذخیره کردن در فایل هستیم. کاربرد ObjectInputStream هم بازگرداندن این فایلها به شی مورد نظر میباشد.
این بار تصویری که در پوشه drawable قرار دارد را به عنوان فایل ذخیره و سپس آن را در اپلیکیشن نمایش خواهیم داد. از کلاس BitmapFactory برای وارد کردن عکس از پوشه drawable به داخل مموری (اپلیکیشن) و همچنین وارد کردن فایل تصویر به داخل مموری استفاده شده است. Bitmap.compress# برای تبدیل کردن تصویر موجود در مموری به بایتهای قابل ذخیره در فایل استفاده میشود.
val imageFile = File(filesDir, "image.jpeg") val fos = FileOutputStream(imageFile) val bos = BufferedOutputStream(fos) val bitmap = BitmapFactory.decodeResource(resources, R.drawable.profile) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos) bos.close() bitmap.recycle() val fis = FileInputStream(imageFile) val bis = BufferedInputStream(fis) val imageBitmap = BitmapFactory.decodeStream(bis) bis.close() image_view.setImageBitmap(imageBitmap)
با استفاده از Extension های موجود بر روی File میتوانید عملیات I/O را کم دردسرتر انجام دهید.
file.bufferedWriter().use { out -> out.write("Hell World!") out.newLine() out.write("Hello Kotlin!") } val br = file.bufferedReader() val content = br.readText() br.close()
در کاتلین هنگام استفاده از بلاک use دیگر نیازی به بستن (close) استریم نداریم و به صورت خودکار بسته خواهد شد.
اگر نیاز به اضافه کردن کاراکتر به فایل موجود در دیسک داشتم چکار کنم؟
//true = perform appending FileOutputStream(file, true).bufferedWriter().use { out -> out.newLine() out.write("Hello World!") } //not recommanded, uses no buffer //appendText() uses .use{}, so closes itself. file.appendText("Append this text to file.")
در این مقاله من از ذخیره بیدردسر فایل در حافظه Internal Storage استفاده کردم، این حافظه برای اپلیکیشن شما خصوصی میباشد و سایر اپلکیشنها از جمله File Manager هم به آن دسترسی ندارد. برای مشاهده فایلها میتوان از طریق Device File Explorer اندروید استادیو اقدام کنیم:
View -> Tool Windows -> Device File Explorer
بعد از باز شدن پنجره این قسمت در سمت راست صفحه به مسیر زیر بروید:
data -> data -> com.example.app -> files
به جای com.example.app نام پکیج برنامه خود را جستجو کنید.