Simge görünürlüğünü kontrol etme

Simge görünürlüğünü kontrol etmek APK boyutunu azaltabilir, yükleme sürelerini iyileştirebilir ve diğer geliştiricilerin uygulama ayrıntılarına yanlışlıkla bağımlı olmaktan kaçınmasına yardımcı olabilir. Bunu yapmanın en güçlü yolu sürüm komut dosyalarını kullanmaktır.

Sürüm komut dosyaları, -fvisibility=hidden öğesinin daha etkili bir biçimi olarak kullanılabilen bir ELF bağlayıcı özelliğidir. Daha ayrıntılı bir açıklama için aşağıdaki Avantajlar bölümüne bakın veya sürüm komut dosyalarını projenizde nasıl kullanacağınızı öğrenmek için okumaya devam edin.

Yukarıda bağlantısı verilen GNU dokümanlarında ve bu sayfadaki diğer birkaç yerde "simge sürümleri" referanslarını göreceksiniz. Bunun nedeni, bu dosyaların asıl amacının, kitaplıklarda hata uyumluluğunu korumak için bir simgenin birden çok sürümünün (genellikle bir işlev) tek bir kitaplıkta var olmasına izin vermek olmasıdır. Android bu kullanımı da destekler, ancak genellikle yalnızca işletim sistemi kitaplığı satıcıları tarafından kullanılır. Hatta targetSdkVersion, daha bilinçli bir etkinleştirme işlemiyle aynı avantajları sunduğundan bunları Android'de bile kullanmıyoruz. Bu belgenin konusu için, "simgesel versiyon" gibi terimlere kafa yormayın. Aynı sembolün birden çok sürümünü tanımlamıyorsanız "sembolü sürümü", dosyada sembollerin keyfi olarak adlandırılmış bir gruplandırılmasıdır.

Uygulama geliştirici değil de kitaplık yazarıysanız (arayüzünüz C/C++ veya Java/Kotlin ise ve doğal kodunuz yalnızca bir uygulama ayrıntısıysa) Ara katman tedarikçi firmaları için öneriler bölümünü de okumayı unutmayın.

Sürüm komut dosyası yaz

İdeal durumda, yerel kod içeren bir uygulama (veya AAR), tüm bağımlılıklarının statik olarak bu tek kitaplığa bağlı olduğu tam olarak bir paylaşılan kitaplık içerir ve bu kitaplığın herkese açık arayüzünün tamamı JNI_OnLoad olur. Böylece, bu sayfada açıklanan avantajların olabildiğince geniş bir kapsamda uygulanması sağlanır. Bu durumda, kitaplığın libapp.so olarak adlandırıldığını varsayarsak aşağıdaki içerikleri içeren bir libapp.map.txt dosyası oluşturun (adın eşleşmesi gerekmez ve .map.txt son eki yalnızca bir kuraldır).

# The name used here also doesn't matter. This is the name of the "version"
# which matters when the version script is actually used to create multiple
# versions of the same symbol, but that's not what we're doing.
LIBAPP {
  global:
    # Every symbol named in this section will have "default" (that is, public)
    # visibility. See below for how to refer to C++ symbols without mangling.
    JNI_OnLoad;
  local:
    # Every symbol in this section will have "local" (that is, hidden)
    # visibility. The wildcard * is used to indicate that all symbols not listed
    # in the global section should be hidden.
    *;
};

Uygulamanızda birden fazla paylaşılan kitaplık varsa kitaplık başına bir sürüm komut dosyası eklemeniz gerekir.

JNI_OnLoad ve RegisterNatives() kullanmayan JNI kitaplıkları için JNI yöntemlerinin her birini JNI karmaşık adlarıyla listeleyebilirsiniz.

JNI olmayan kitaplıklar (genellikle JNI kitaplıklarının bağımlılıkları) için API yüzeyinizin tamamını belirtmeniz gerekir. Arayüzünüz C yerine C++ ise extern "C++" { ... } değerini bir başlık dosyasında kullandığınız gibi sürüm komut dosyasında da kullanabilirsiniz. Örnek:

LIBAPP {
  global:
    extern "C++" {
      # A class that exposes only some methods. Note that any methods that are
      # `private` in the class will still need to be visible in the library if
      # they are called by `inline` or `template` functions.
      #
      # Non-static members do not need to be enumerated as they do not have
      # symbols associated with them, but static members must be included.
      #
      # The * exposes all overloads of the MyClass constructor, but note that it
      # will also expose methods like MyClass::MyClassNonConstructor.
      MyClass::MyClass*;
      MyClass::DoSomething;
      MyClass::static_member;

      # All members/methods of a class, including those that are `private` in
      # the class.
      MyOtherClass::*;
      #

      # If you wish to only expose some overloads, name the full signature.
      # You'll need to wrap the name in quotes, otherwise you'll get a warning
      # like like "ignoring invalid character '(' in script" and the symbol will
      # remain hidden (pass -Wl,--no-undefined-version to convert that warning
      # to an error as described below).
      "MyClass::MyClass()";
      "MyClass::MyClass(const MyClass&)";
      "MyClass::~MyClass()";
    };
  local:
    *;
};

Oluşturma sırasında sürüm komut dosyasını kullanın

Derleme sırasında sürüm komut dosyası bağlayıcıya iletilmelidir. Aşağıdaki derleme sisteminiz için uygun adımları uygulayın.

CMake

# Assuming that your app library's target is named "app":
target_link_options(app
    PRIVATE
    -Wl,--version-script,${CMAKE_SOURCE_DIR}/libapp.map.txt
    # This causes the linker to emit an error when a version script names a
    # symbol that is not found, rather than silently ignoring that line.
    -Wl,--no-undefined-version
)

# Without this, changes to the version script will not cause the library to
# relink.
set_target_properties(app
    PROPERTIES
    LINK_DEPENDS ${CMAKE_SOURCE_DIR}/libapp.map.txt
)

ndk-build

# Add to an existing `BUILD_SHARED_LIBRARY` stanza (use `+=` instead of `:=` if
# the module already sets `LOCAL_LDFLAGS`):
LOCAL_LDFLAGS := -Wl,--version-script,$(LOCAL_PATH)/libapp.map.txt

# This causes the linker to emit an error when a version script names a symbol
# that is not found, rather than silently ignoring that line.
LOCAL_ALLOW_UNDEFINED_VERSION_SCRIPT_SYMBOLS := false

# ndk-build doesn't have a mechanism for specifying that libapp.map.txt is a
# dependency of the module. You may need to do a clean build or otherwise force
# the library to rebuild (such as by changing a source file) when altering the
# version script.

Diğer

Kullandığınız derleme sisteminde sürüm komut dosyaları için açık destek varsa bunu kullanın.

Aksi takdirde aşağıdaki bağlayıcı işaretlerini kullanın:

-Wl,--version-script,path/to/libapp.map.txt -Wl,--no-version-undefined

Bu seçeneklerin nasıl belirtileceği derleme sisteminize bağlıdır ancak genellikle LDFLAGS veya benzer bir seçenek bulunur. path/to/libapp.map.txt, bağlayıcının mevcut çalışma dizininden çözülebilir olmalıdır. Genellikle mutlak yol kullanmak daha kolaydır.

Derleme sistemi kullanmıyorsanız veya derleme sistemiyle ilgilenen bir geliştiriciyseniz ve sürüm komut dosyası desteği eklemek istiyorsanız bu işaretçiler, derleme sırasında değil, bağlama sırasında clang (veya clang++) parametresine iletilmelidir.

Avantajlar

Sürüm komut dosyası, kitaplıktaki görünür simge kümesini en aza indirdiğinden APK boyutu, sürüm komut dosyası kullanılırken iyileştirilebilir. Bağlayıcı, arayanlar için tam olarak hangi işlevlerin erişilebilir olduğunu belirterek bağlayıcıya, erişilemeyen tüm kodu kitaplıktan kaldırabilir. Bu işlem, ölü kod kaldırma işleminin bir türüdür. Bağlantılayıcı, görünür bir sembolün kitaplığın herkese açık arayüzünün bir parçası olduğunu varsayması gerektiğinden, işlev hiç çağrılmamış olsa bile gizli olmayan işlevin (veya diğer sembolün) tanımını kaldıramaz. Simgelerin gizlenmesi, bağlayıcının çağrılmayan işlevleri kaldırmasına olanak tanır ve kitaplığın boyutunu azaltır.

Kitaplık yükleme performansı da benzer nedenlerle iyileştirildi: Görünür simgeler yer değiştirebilir olduğundan bu simgeler için yer değiştirme gerekir. Bu, neredeyse hiçbir zaman istenen davranış değildir ancak ELF spesifikasyonu tarafından zorunlu kılındığı için varsayılan olarak kullanılır. Ancak bağlayıcı, hangi sembollerin (varsa) araya yerleştirilmesini istediğinizi bilmediği için her görünür sembol için yeniden yerleştirme işlemi oluşturmalıdır. Bu sembollerin gizlenmesi, bağlayıcının doğrudan atlamalar için bu taşıma işlemlerini atlamasına olanak tanır. Bu da dinamik bağlayıcının kitaplıkları yüklerken yapması gereken iş miktarını azaltır.

API yüzeyinizi açık bir şekilde numaralandırmanız, ayrıntılar görünür olmayacağından, kitaplıklarınızı kullananların kitaplığınızın uygulama ayrıntılarına yanlışlıkla bağlı olmalarını da önler.

Alternatiflerle karşılaştırma

Sürüm komut dosyaları, -fvisibility=hidden veya işlev başına __attribute__((visibility("hidden"))) gibi alternatiflerle benzer sonuçlar sunar. Bu üç yaklaşımın tümü, bir kitaplığın hangi simgelerinin diğer kitaplıklar ve dlsym tarafından görülebileceğini kontrol eder.

Diğer iki yaklaşımın en büyük dezavantajı, yalnızca oluşturulmakta olan kitaplıkta tanımlanan simgeleri gizlemeleridir. Simgeleri kitaplığın statik kitaplık bağımlılıklarından gizleyemezler. Bunun bir fark yarattığı en yaygın durum libc++_static.a kullanılırken ortaya çıkar. Derlemeniz -fvisibility=hidden kullanıyor olsa bile kitaplığın kendi simgeleri gizlenirken libc++_static.a'ten dahil edilen tüm simgeler kitaplığınızın herkese açık simgeleri olur. Buna karşılık sürüm komut dosyaları, kütüphanenin herkese açık arayüzünü açıkça kontrol etme olanağı sunar. Simge, sürüm komut dosyasında açıkça görünür olarak listelenmezse gizlenir.

Diğer fark hem avantaj hem de dezavantaj olabilir: Kitaplığın herkese açık arayüzü, sürüm komut dosyasında açıkça tanımlanmalıdır. JNI kitaplıkları için bu aslında önemsizdir. Çünkü JNI kitaplığı için gereken tek arayüz JNI_OnLoad'tür (RegisterNatives() ile kaydedilen JNI yöntemlerinin herkese açık olması gerekmediğinden). Büyük bir herkese açık arayüze sahip kütüphaneler için bu, ek bir bakım yükü olabilir ancak genellikle buna değerdir.