10 Aralık 2011 Cumartesi

Derinlemesine Android

Merhaba,

Bugün birer android geliştirici olarak kullandığımız bu platformun Türkçe kaynaklarda bariz eksikliğinin göründüğüne inandığım temellerine inmeye başlayacağız. Pek tabi konu Android'in temelleri olunca, ister istemez linux literatüründen de bazı kavramlara girmemiz kaçınılmaz olacak. Elimden geldiğince yalın dille anlatmaya çalışacağım bu yazı umuyorum tüm Android ile ilgilenen yazılımcı arkadaşlar için faydalı olur. Evet herşey hazırsa, kemerlerinizi takın ve arkanıza yaslanın keza android'in temellerine olan yolculuğumuz şu andan itibaren başlamak üzere.

Android Stack Yapısı

Android; mobil cihazlar için geliştirilmiş, genel manada 4 farklı katmandan oluşan bir işletim platformudur. Bu katmanların en altında, platformun abstraction layer'ı olma görevini de üstlenecek şekilde konumlandırılmış olan linux kernel bulunmaktadır. HAL'de (hardware abstraction layer) linux kernel'in kullanılmasının temelde üç önemli amacı vardır.
  1. Fragmantasyonun android'de fazla olması (Andy Rubin'in belirttiğine göre her gün 500.000 yeni androidli cihaz piyasaya çıkmaktadır) ve linux kernel'in farklı donanımlarda kolayca compile edilebilmesi münasebetiyle yaşanması muhtemel uyum problemlerinin önüne geçmek. Bu sıkıntıyı gidermek maksadıyla bir çok farklı donanıma ait sürücülerin linux kernel içerisinde implemente edilmiş halde bulunduğunu söyleyebiliriz.
  2. Güvenlik, hız, proses ve network yönetimi konularında yadsınmaz başarısı.
  3. Linux kernel'in de hali hazırda açık kaynak olması.
Belirtmeden geçmeyelim, android linux kernel'i kullanmaktadır, linux değildir. Ayrıca linux'ın tüm standartlarını içermez. Bununla beraber Android için linux kernel'e ilave edilmiş alarm, power management, kernel debugger, low memory killer, ashmem, binder ve logger gibi nesneler mevcuttur. Bu noktada Binder'a biraz değinmekte fayda var; Android'de uygulamalar ve servisler ayrı prosesler üzerinde çalışırlar yani bir proses başka bir prosesin memory'sine erişemez ancak bunun yanında aralarında haberleşme ve data paylaşımına da ihtiyaç duyarlar. İşte Android'de iki proses arasında data aktarımını-mesaj gönderimini sağlayan yani interproses communication çağrıları (IPC Call) yöneten yapı Binder'dır.

Android'de iki farklı prosesin birbirleriyle haberleşmesini Binder sağlar

İkinci katmanda, android geliştiricilerinin çok nadiren direk olarak erişmeye çalıştığı genel manada ise üst katmanların ihtiyaç duyacağı işlevselliklerin yerine getirilebilmesini sağlayacak kütüphanelerin (databasesel ilişkilerin yönetilmesine imkan veren sqlite, opengl için 2D-3D grafik işleme, bitmap ve vektör font render ederken kullanılan freetype, html render etmede kullanılan webkit gibi) implemente edildiği native libraries bulunmaktadır. 

Dalvik Virtual Machine (DVM)
Android uygulamalarını compile ederken Android için özel olarak yazılmış Dalvik Virtual Machine kullanılır. Uygulamalar genellikle Java ile geliştirildiğinden, kodlar önce JVM (Java Virtual Machine) ile bytecode'a compile edilir (.java => .class). Bu işlem sonrası JVM ile uyumlu hale gelen .class dosyaları uygulama çalıştırılmaya başlamadan önce bir de Android'in anlayabileceği ve çalıştırabileceği Dalvik compiled executable file olan .dex formatına çevrilir. Aşağıdaki resimde, JVM ile çalışan cihaz ile Android arasındaki fark gösterilmektedir. Burada şu soruyu sorabilirsiniz "neden direk Dalvik byte code'a çevrilmiyor da önce Java byte code'a daha sonra Dalvik byte code'a çevriliyor?" Bu sorunun temelde iki nedeni var.
  1. DVM registerbased virtual machine JVM ise stack-based virtual machine'dir. Stack-based vmler register-based vmler'e göre ham kodu generate etmede daha hızlı ve kolaydırlar. Register-basedler ise  yüksek ölçekte optimize edilmiş kodun generate edilmesinde ve hızlı implementasyon yaratımlarında daha başarılıdırlar. Dolayısıyla compile etme işleminin tekrar tekrar yapılması yerine önceden jvm ile makinenin anlayabileceği dile çok yakın şekilde çevrilmiş byte code'tan dvm'nin ürettiği dex tipine çevirim kullanılır.
  2. DVM, .NET framework'ün virtual machine'i olan CLR (Common Language Runtime) mantığında çalışır. CLR, IL (intermediate language) adında ara bir dil vasıtasıyla uygulamayı hem dilden hem de işletim sisteminden bağımsız hale getirir. DVM'de de mantık aynı şekildedir, her ne kadar teoride gibi gözükse de Phyton'la yazdığınız kodu dalvik ile compile edebiliyorsanız android bu uygulamanızı başarıyla çalıştırabilecek demektir.
Android'de java kodu 2 kere compile edilmiş olur

Dex dosyasının bir diğer oluşturulma amacı hafıza ve işlemci hızının daha optimum seviyelere indirgenmesinin sağlanmasıdır keza Dalvik Virtual Machine'in geliştiricisi Dan Bornstein 2008 yılında yaptığı bir sunumda %100 işlemci kaynaklarını kullanan sıkıştırılmamış (uncompressed) bir java kodunun, sıkıştırıldığında %56'lara, Dalvik'li sıkıştırılmamış halinin ise %48'lere kadar indiğini ifade etmiştir. Yani arada neredeyse yarı yarıya bir fark vardır ki bu bir çok açıdan sınırlı olan mobil cihazlar için çok önemli bir ayrıntıdır. Bu iyileştirmeyi sağlamada en büyük etkenlerden biri -DVM'nin JVM'den en büyük farkı olarak da gösterilmesine neden olan- önceden de belirttiğimiz gibi DVM'nin stack based yerine register based olarak geliştirilmesinde yatmaktadır.

 Ayrıca JVM, aygıtın tipine bakmaksızın tüm cihazlar için bir tanedir (one size fits all). Yani bir mobil cihaz için kullanılan JVM ile mobil olmayan bambaşka bir cihaz (örneğin süper bilgisayar) için kullanılan JVM'de hiç bir farklılık yoktur. Bunun yanında DVM spesifik olarak mobil cihazlar için geliştirildiğinden JVM ile oluşan JAR dosyasından çok daha küçük ve mobil cihazın ihtiyaç duymayacağı (kaynak kod ile ilgili bir sürü gerekli gereksiz bilgi -verbose data- jar dosyasının içerisine yazılmaktadır) bilgilerin atılarak derlendiği bir dosya çıktısı oluşturmaktadır. Tüm bu teknik özelliklerin yanında Google'ı DVM'yi yazmaya iten nedenlerden bir diğeri de Oracle ile geçmişte yaşamış olduğu lisans problemleridir.

Hepimizin bildiği gibi Android bir açık kaynak kod geliştirme platformudur ve açık kaynak kod projelerinde, geliştiricilerin platformun tüm öğelerinden istedikleri miktarda erişim sağlaması birincil amaçtır. Bu amaç doğrultusunda, oldukça kapsamlı ve geniş kütüphaneler hazırlanarak kullanıcıların maksimum fayda edinebilecekleri bir altyapı oluşturulması önemlidir. Stack'te bulunan üçüncü kısım yani application framework (uygulama altyapısı) bu ihtiyaç için oluşturulmuştur. Application framework'te bulunan nesneler, geliştiricilerin native librarylere ulaşmasında kilit rol oynarlar. Bu nesneler, native'de bulunan kütüphane elemanlarıyla iletişime geçerek, normal şartlarda ancak taklalar atarak ulaşabileceğimiz bilgileri (örneğin bir koordinat bilgisi) son derece basit bir şekilde elde etmemize imkan verirler. Kısaca kodlarımızda kullandığımız neredeyse tüm nesneler burada bulunan nesnelerin soyutlandırılmış halleridir.

Yapının en üst bölümünde yazmış olduğumuz applicationlar (uygulamalar) bulunmaktadır. Application, bir veya birden çok apk (application package file) adı verilen paketlerin birleşiminden meydana gelir. Apk dosyaları .jar dosyaları gibi sıkıştırılmış dosyalardır. Android platformundaki yükleme ve dağıtım gibi işlevleri yerine getirdikleri için android'teki exe dosyaları şeklinde adlandırılabilirler. Apk dosyaları içerilerinde, yazmış olduğumuz kodları (ki bu kodlar apk oluşumu esnasında derlenerek dex formatına çevrilirler), resource, manifest, sertifika ve diğer uygulamayla ilgili tüm bilgileri barındırırlar.

Güvenlik
Android, Inter-Component Communication (Komponentler Arası İletişim) yapısına göre çalışır. Her komponent tipi sadece kendi tipindekilerle iletişime geçebilir ve yine her uygulama sadece kendisine atanan tekil bir kullanıcı kimliği aracılığıyla kendi linux prosesi üzerinde çalıştırılır. Bu sayede uygulamadan kaynaklanması muhtemel kusurlar nedeniyle oluşacak hasarlar önlenmiş olur. Yine her uygulama diğer uygulamalardan kendi izolasyonunu sağlamak amacıyla veritabanı ve diğer konfigürasyon arabirimlerinin belirtilmiş olduğu bir sandbox (çocuk parklarında çocukların güvenliğini sağlamak amacıyla oluşturulan kum havuzu gibi düşünebilirsiniz) dosya sistemine sahiptir. Bu izolasyon yapısı sayesinde, uygulama izin verdiğini belirtmedikçe asla diğer uygulamalar tarafından erişilemez. (Google mühendislerinden Dianne Hackborn'ün konuyla ilgili açıklaması: Android had a number of very different original design goals than iOS did. A key goal of Android was to provide an open application platform, using application sandboxes to create a much more secure environment that doesn’t rely on a central authority to verify that applications do what they claim. To achieve this, it uses Linux process isolation and user IDs to prevent each application from being able to access the system or other application in ways that are not controlled and secure. https://plus.google.com/105051985738280261832/posts/XAZ4CeVP6DC)

Her uygulama kendisine ait tekil bir kimlik bilgisi ile çalıştırılarak çevresinden izole edilmiş olur.

Aslında olan biteni şöyle bir analojiyle açıklayabiliriz; bir otel düşünün, içinde kalan her insan için ayrı bir oda tesis edilmiş olsun. Güvenlikten sorumlu bir otel görevlisi herkes için ayrı ayrı yetkilerinin yazılı olduğu bir yaka kartı hazırlıyor ve hazırlanan bu yaka kartlarının takılması zorunlu. İnsanlar birbirleriyle görüşmek istediğinde otel görevlisi görüşüp görüşemeyeceklerine bu yetki kartlarına bakarak onay veriyor ya da vermiyor. 

Android: otel
Otel odaları: tekil proses idler
İnsanlar: uygulama ve komponentler
Güvenlikten sorumlu otel görevlisi: referans monitör
Yaka kartları: permission labels (yetki etiketleri)

Android'de Başlangıç Mekanizması (Startup)
  1. Bootloader linux kernel'i yükler ve init prosesi başlatır. (/system/init)
  2. Init, çoğu /system/bin adresinde bulunan, android native programda implemente edilmiş bazı servisleri (usbd [usb bağlantılarını yönetmek için], adbd [android debug bridge-adb bağlantılarını yönetmek için], debuggerd [dump memory gibi debug proses taleplerini yönetmek için], rild [radio interface layer daemon - radyo ile bağlantıyı yönetmek için], vold, netd, installd, qemud) başlatır. Yeri gelmişken belirtelim unix'te (veya multitask çalışan diğer işletim sistemlerinde) init aşamasında arka planda çalıştırılmaya başlanan servislere daemon denir ve genelde sona koyulan d ile belirtilirler.)  
  3. Init, zygote (zigot) prosesi başlatır. Zygote classları yükleyen ve sanal makine için soketten gelecek istekleri dinleyen çekirdek prosestir. Aslında zygote'un yaptığı şey tüm uygulamaları çalışmaya hazır hale getirmektir şöyle ki; bir futbol takımında maç öncesi ilk 11'de oynasın oynamasın tüm futbolcular ısınır değil mi? Neden? Çünkü her an görev alma olasılığı mevcuttur. Aniden maçta oynanması istendiğinde hocam 2 dk bekleyin ben bir ısınıp geleyim dediğini düşünsenize. Zygote bu ısınma durumunu sağlar. Tüm uygulamaları, ileride çalışsın çalıştırılmasın farketmeksizin belli bir seviyeye kadar ayağa kaldırır (uykudan uyandırır, android uygulamayı çağırdığında gidip bir de onu uyandırmayla uğraşmaz).
  4.  Init runtime prosesi, runtime proses de service manager'ı başlatır. Servis manager, uygulamanın ihtiyaç duyacağı servisleri yönetecek (TelephonyService, LocationService, vb), gerektiğinde yeni yazılan servisin kaydedileceği, gerektiğinde ise kullanılması gereken servisin çekilebileceği kısacası servislerle ilgili tüm içeriğin yönetiminin sorumlu olduğu yapıdır.
  5. Zygote dalvik vm'yi çalıştırmaya başlarken bir diğer yandan da kendisine system server'ın çalıştırılması yönünde bir istek yollanır, zygote system service için yeni bir sanal makine (vm) kopyalayarak (forking) servisi çalıştırır. Burada forking denen hadiseyi biraz açıklayalım; forking bir diğer anlamıyla daemonizing, programın yeni bir child proses ile arka planda yürütülmesi demektir. Linux'ta yeni bir proses yaratılmak istendiğinde fork() kullanılır. Mitoz bölünme gibi düşünebiliriz keza bütün proseslerin atası Init'tir. Son olarak ekleyelim fork() çalıştırıldığında kopyalanan proses önceki prosesin kaldığı yerden devam eder. Biraz daha basite indirgeyelim; bir proses var, bu prosesten başka bir proses oluşturulması veya bir diğer deyişle child'i oluşturulması gerekiyor ki bunu ancak  fork() ile yapabiliyoruz. fork() geriye bir proses id dönüyor. Bu prosesId > 0 ise parent üzerinden işleme prosesId = 0 ise child üzerinden prosese devam ediliyor. 
  6. System server, surface flinger ve audio flinger'ın bulunduğu native system serverları başlatır. Native sistem sunucuları sistem manager'a IPC servis hedefleri olarak kaydedilir. Bu andan itibaren, çalışma zamanı prosesi (runtime process, service manager, system server (sistem sunucusu) ve native sunucuların hepsi android'in native programı olarak implemente edilmiş olur.
  7. Sistem servisi (system service) window manager, activity manager, location manager gibi tüm yönetim işlemini üstlenen android managed services'i başlatır. Android managed services service manager'a kaydedilir.
yeşiller native kod tarafını, maviler java tarafını göstermektedir.

8 yorum:

  1. Yıllar evveli java kitabınızı okumus biri olarak böyle bir yorumu sizden almak beni onurlandırdı, teşekkür ederim.

    YanıtlaSil
  2. Yanıtlar
    1. Oldukça güzel, yararlı bir araştırma yazısı olmuş. MaşaAllah.

      Sil
  3. güzel yazı dostum..teşekkürler

    YanıtlaSil
  4. Teşekkürler, oldukça faydalı bir yazı olmuş.

    YanıtlaSil