خواندن/نوشتن فایل در اندروید (Android Storage I/O)

مقدمهدر این پست قصد یادگیری مفهومی و مناسب از عملیات خواندن/نوشتن (Input/Output) را از رم به دیسک و بالعکس را داریم. احتمالا شما هم در هنگام انجام پروژه‌ها با هرگونه سطح چالش با این مسئله مواجه شده‌اید که نیاز بوده فایلی را در دیسک (مموری کارد) اندروید ذخیره کنید یا آن را وارد اپلیکیشن خود کنید.لیست کلمات و معانی آن از نظر نویسنده:رم = Ramدیسک = Android Memory Cardخواندن/نوشتن = Input/Outputدریافت سمپل کد برای پروژه:لینک گیت‌هابنسخه انگلیسی مقاله:لینک مدیومآشنایی با مفاهیم اولیهمنظور از I/O چیست؟Input = خواندن اطلاعات از دستگاه فیزیکی و انتقال آن به رم Output = انتقال اطلاعات موجود در رم به دستگاه فیزیکی و ذخیره آنتمامی کلاس‌هایی که ما در این پست آموزش استفاده از آن‌ها را یاد می‌گیریم در پکیج java.io قرار گرفته‌اند.دستگاه فیزیکی چیست؟کیبورد، صفحه نمایش و دیسک همگی مثال‌های دستگاه‌های فیزیکی می‌باشند.در این پست هدف ما یادگیری ارتباط بین دیسک و رم می‌باشد.منظور از Stream چیست؟استریم (جریان داده) عبارت است از یک پل ارتباطی که بین دستگاه فیزیکی و رم برای کمک به تبادل اطلاعات صورت می‌گیرد.جاوا از دو نوع استریم پشتیبانی می‌کند:Byte Stream = دریافت و ارسال اطلاعات به صورت بایتCharacter Stream = دریافت و ارسال اطلاعات به صورت نوشتهبرای ذخیره/خواندن فایل‌های نوشتاری (Text) از Character Stream و برای هر نوع فایل دیگر از جمله عکس‌ها ، ویدیو و ... از Byte Stream استفاده می‌کنیم.کلاس‌های زیر مجموعه جریان‌های داده در نمودار بالا هر چهار کلاس نمایش داده شده (مستطیل‌ها) از نوع Abstract می‌باشند.مفهوم Abstract در Java و Kotlin مربوط به کلاسی می‌باشد که توانایی ایجاد نمونه (شی) از خود نداشته و حتما نیاز است که کلاسی از آن ارث‌بری (Extend) کند.با توجه به مفهوم بالا نیاز داریم که از Subclass های آن‌ها استفاده کنیم.FileReader = خواندن فایل نوشتاریFileWriter = ذخیره فایل نوشتاریFileInputStream = خواندن فایلFileOutputStream = ذخیره فایلکلاس File برای کار با فایل‌ها و پوشه‌هاکلاسی که به شما تنها اجازه دسترسی به اطلاعات فایل‌های درون دیسک را می‌دهد، اطلاعاتی همچون محل ذخیره، نوع و حجم آن. می‌توان این کلاس را مثل یک کاغذ کادویی به دور یک هدیه در نظر گرفت، کاغذ کادویی شامل شکل و اندازه، محل قرارگیری کادو می‌باشد ولی خود هدیه نیست.پیش به سوی انجام عملیاتذخیره‌سازی فایل نوشتاری ... ولی با یک چالش!Github Gistval file = File(filesDir, &quothello.txt&quot) val fw = FileWriter(file) fw.write(&quotHello&quot) fw.write(&quot\n&quot) fw.write(&quotWorld!&quot) fw.close()دلیل بستن FileWriter چیست؟در هنگام کار با کلاس‌های I/O برای نهایی شدن عملیات خواندن/نوشتن و آزاد شدن منابع تحت تسلط آن‌ها حتما باید آن را ببندید، در غیر این صورت عملیات نهایی نمی‌شود و در نتیجه فایل شما ساخته نمی‌شود.در صورت نیاز به انجام عملیات بدون بستن استریم می‌توانید از فانکشن flush به جای close استفاده کنید.در صورتی که نحوه دسترسی به فایل ذخیره شده در دیسک را نمی‌دانید، می‌توانید با استفاده از راهنمای انتهای مقاله آن را یاد بگیرید.در اینجا یک مشکل سرعت انجام عملیات داریم! برای ذخیره سازی تک تک کاراکتر‌های‌های نیاز است که به سیستم‌عامل دستور داده شود که آن را بر روی دیسک (مموری کارد) ذخیره کند.مفهوم Buffer چیست؟بافر قسمتی از رم می‌باشد که اطلاعات را قبل از خواندن/نوشتن در خود ذخیره می‌کند تا سرعت عملیات را افزایش دهد. وقتی بافر پر شد حالا تنها در یک قدم اطلاعات ارسال/دریافت می‌شوند و دیگر نیازی به فراخوانی‌های مجدد سیستم‌عامل نیست. بدون استفاده از Buffer برای ذخیره‌سازی 500 کاراکتر نیاز به 500 فراخوانی سیستم‌عامل وجود دارد.کلمه Buffer در معنای لغوی به معنای میانگیر می‌باشد.BufferedWriter = برای ذخیره کردن فایل‌های نوشتاریBufferedReader = برای خواندن فایل‌های نوشتاریBufferedOutputStream = برای ذخیره فایل‌هاBufferedInputStream = برای خواندن فایل‌هاذخیره فایل نوشتاری ... روش بهینهGithub Gistval file = File(filesDir, &quothello.txt&quot) val fw = FileWriter(file) val bw = BufferedWriter(fw) bw.write(&quotHello&quot) bw.newLine() bw.write(&quotWorld!&quot) bw.close()چرا FileWriter را به عنوان ورودی BufferedWriter دادیم؟کلاس‌های استریم پکیج java.io یا از نوع اتصالی (Connection) و یا از نوع زنجیری (Chain) می‌باشند. Connection Stream = کلاس‌هایی که مستقیما به منبع اطلاعات متصل می‌شوندChain Stream = کلاس‌هایی که به استریم‌های اتصالی زنجیر می‌شوند تا تغییری در عملیات صورت گیردنحوه کار کرد استریم‌ها با یکدیگربا بستن بالاترین استریم باقی نیز به صورت خودکار بسته خواهند شد، پس با بستن BufferedWriter دیگر نیازی به بستن FileWriter به صورت جداگانه نداریم.جاوا از الگوی طراحی‌ای (Design Pattern) به نام Decorator برای اتصال این کلاس‌ها به هم استفاده می‌کند. با ترکیب این کلاس‌ها با یکدیگر می‌توان کارایی کلاس‌های استریم را تغییر داد.خواندن فایل نوشتاریGihub Gistval file = File(filesDir, &quothello.txt&quot) val fr = FileReader(file) val br = BufferedReader(fr) var line: String? = &quot&quot val sb = StringBuilder() while (br.readLine().also { line = it } != null) { sb.append(line + &quot\n&quot) } bis.close()توضیحاتی در مورد این کدکلاس BufferReader به ما نوشته‌ها رو خط به خط میده در نتیجه در داخل یک حلقه تا زمانی که خط‌ها به پایان نرسیدن اون‌ها رو به StringBuilder اضافه می‌کنیم.فانکشن .also هم یکی از Scope Function های کاتلین هستش که بعد از انجام کاری (خواندن خط در این حالت) کار دیگه ای رو هم انجام می‌ده بدون اینکه تغییری روی مقدار قبلی بزاره.خط رو بخون و همچنین (also) اون رو داخل متغییر line ذخیره کن بدون اینکه تاثیری روی مقدارش بزاری و حالا چک کن خطی که خوندی null نباشه.دیاگرام کلاس‌های مهم I/Oکلاس‌های Byte Streamکلاس‌های Character Streamخواندن/نوشتن Object ها (Serialization/Deserialization)در این مرحله با Byte Stream ها آشنا می‌شویم.منظور از Serialization و Deserialization چیست؟به عملیات تبدیل یک شی (Object) به فایل برای ذخیره وضعیت آن شی Serialization گفته می‌شود.در صورتی که فایل را به حالت شی (Object) بودن آن برگردانیم این عمل Deserialization نامیده می‌شود.در پلتفرم اندروید به جای Serialization استفاده از Parcelable برای افزایش سرعت این فرآیند توصیه می‌شود.val file = File(filesDir, &quothello.txt&quot) val person = Person(&quotKeanu Reeves&quot, 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 هم بازگرداندن این فایل‌ها به شی‌ مورد نظر می‌باشد.کار با Bitmap (تصاویر)این بار تصویری که در پوشه drawable قرار دارد را به عنوان فایل ذخیره و سپس آن را در اپلیکیشن نمایش خواهیم داد. از کلاس BitmapFactory برای وارد کردن عکس از پوشه drawable به داخل مموری (اپلیکیشن) و همچنین وارد کردن فایل تصویر به داخل مموری استفاده شده است. Bitmap.compress# برای تبدیل کردن تصویر موجود در مموری به بایت‌های قابل ذخیره در فایل استفاده می‌شود.Github Gistval imageFile = File(filesDir, &quotimage.jpeg&quot) 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)انجام عملیات I/O به سبک Kotlin Extensionsبا استفاده از Extension های موجود بر روی File می‌توانید عملیات I/O را کم دردسرتر انجام دهید.Github Gistfile.bufferedWriter().use { out -> out.write(&quotHell World!&quot) out.newLine() out.write(&quotHello Kotlin!&quot) } val br = file.bufferedReader() val content = br.readText() br.close()در کاتلین هنگام استفاده از بلاک use دیگر نیازی به بستن (close) استریم نداریم و به صورت خودکار بسته خواهد شد.اگر نیاز به اضافه کردن کاراکتر به فایل موجود در دیسک داشتم چکار کنم؟Github Gist//true = perform appending FileOutputStream(file, true).bufferedWriter().use { out -> out.newLine() out.write(&quotHello World!&quot) } //not recommanded, uses no buffer //appendText() uses .use{}, so closes itself. file.appendText(&quotAppend this text to file.&quot)راهنمادسترسی به محل ذخیره فایل مثال‌هادر این مقاله من از ذخیره بی‌دردسر فایل در حافظه Internal Storage استفاده کردم، این حافظه برای اپلیکیشن شما خصوصی می‌باشد و سایر اپلکیشن‌ها از جمله File Manager هم به آن دسترسی ندارد. برای مشاهده فایل‌ها می‌توان از طریق Device File Explorer اندروید استادیو اقدام کنیم:View -> Tool Windows -> Device File Explorerبعد از باز شدن پنجره این قسمت در سمت راست صفحه به مسیر زیر بروید:data -> data -> com.example.app -> filesبه جای com.example.app نام پکیج برنامه خود را جستجو کنید.