abstractArrow
abstractArrow
خواندن ۸ دقیقه·۵ سال پیش

خواندن/نوشتن فایل در اندروید (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 Gist

val 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 Gist

val 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 Gist

val 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
کلاس‌های Byte Stream


کلاس‌های Character 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 Gist

val 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 Gist

file.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 نام پکیج برنامه خود را جستجو کنید.

androidاندرویدjavakotlinio
بیشترین طلاها از ذهن افراد بیرون کشیده می‌شود، نه معادن
شاید از این پست‌ها خوشتان بیاید