天天看點

元件化架構設計之apt編譯時期自動生成代碼&動态類加載(二)一、apt編譯時期自動生成代碼二、Android動态加載技術基礎之類加載(ClassLoader)最後

11/12号文檔資料已全面更新!;《【阿裡P7】移動網際網路架構師進階教程+BAT面試題》,點選下方連結前往領取: 【阿裡P7】移動網際網路架構師進階進階教程+BAT面試題 本篇文章将繼續從以下兩個内容來介紹元件化架構設計:

  • apt編譯時期自動生成代碼
  • Android動态加載技術基礎之類加載(ClassLoader)

一、apt編譯時期自動生成代碼

第一步

建立一個android項目。

第二步

建立立一個java的Module。注意是javalib。這個lib用來專門寫注解就好。

這個lib裡面就先放一個注解,叫TestAnno。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface TestAnno {
}           

RetentionPolicy.CLASS表示編譯時候注解。你需要關系的就是@Target(ElementType.TYPE)這個type是類的注解,可以有方法的,屬性的等等。

然後這個javalib的gradle檔案要這麼寫。

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"           

注解庫弄好了,在弄建立一個java lib 叫inject_comiler。這個是就是核心代碼了,在編譯時候,執行這個個庫裡面的代碼,然後 生成代碼。這個工程 三個類。一個是注解注解處理器的核心。一個是定義生成java檔案的,方法拼接。還有一個就是常量包名類名了。

先看這個的gradle檔案。

apply plugin: 'java'
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.google.auto.service:auto-service:1.0-rc2'//谷歌的幫助我們快速實作注解處理器
    compile project(':inject_annotation')//自己定義的注解的java lib
    compile 'com.squareup:javapoet:1.7.0'//用來生成java檔案的,避免字元串拼接的尴尬
}           
//這個注解是谷歌提供了,快速實作注解處理器,會幫你生成配置檔案啥的 。直接用就好
@AutoService(Processor.class)
public class ActivityInjectProcesser extends AbstractProcessor {
    private Filer mFiler; //檔案相關的輔助類
    private Elements mElementUtils; //元素相關的輔助類  許多元素
    private Messager mMessager; //日志相關的輔助類

    private Map<String, AnnotatedClass> mAnnotatedClassMap;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        mAnnotatedClassMap = new TreeMap<>();
    }

//這個方法是核心方法,在這裡處理的你的業務。檢測類别參數,生成java檔案等
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mAnnotatedClassMap.clear();

        try {
            processActivityCheck(roundEnv);
        } catch (Exception e) {
            e.printStackTrace();
            error(e.getMessage());
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                annotatedClass.generateActivityFile().writeTo(mFiler);
            } catch (Exception e) {
                error("Generate file failed, reason: %s", e.getMessage());
            }
        }
        return true;
    }


    private void processActivityCheck(RoundEnvironment roundEnv) throws IllegalArgumentException, ClassNotFoundException {
        //check ruleslass forName(String className
        for (Element element : roundEnv.getElementsAnnotatedWith((Class<? extends Annotation>) Class.forName(TypeUtil.ANNOTATION_PATH))) {
            if (element.getKind() == ElementKind.CLASS) {
                getAnnotatedClass(element);
            } else
                error("ActivityInject only can use  in ElementKind.CLASS");
        }
    }

    private AnnotatedClass getAnnotatedClass(Element element) {
        // tipe . can not use chines  so  ....
        // get TypeElement  element is class's --->class  TypeElement typeElement = (TypeElement) element
        //  get TypeElement  element is method's ---> TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        TypeElement typeElement = (TypeElement) element;
        String fullName = typeElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(typeElement, mElementUtils, mMessager);
            mAnnotatedClassMap.put(fullName, annotatedClass);
        }
        return annotatedClass;
    }


    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    //這個個方法傳回你要處理注解的類型
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(TypeUtil.ANNOTATION_PATH);
        return types;
    }

    private void error(String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }

    private void log(String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, String.format(msg, args));
    }
}           

然後是生成java檔案的輔助類。

public class AnnotatedClass {

    private TypeElement mTypeElement;//activity  //fragmemt
    private Elements mElements;
    private Messager mMessager;//日志列印

    public AnnotatedClass(TypeElement typeElement, Elements elements, Messager messager) {
        mTypeElement = typeElement;
        mElements = elements;
        this.mMessager = messager;
    }


    public JavaFile generateActivityFile() {
        // build inject method
        MethodSpec.Builder injectMethod = MethodSpec.methodBuilder(TypeUtil.METHOD_NAME)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.get(mTypeElement.asType()), "activity", Modifier.FINAL);
        injectMethod.addStatement("android.widget.Toast.makeText" +"(activity, $S,android.widget.Toast.LENGTH_SHORT).show();", "from build");
        //generaClass
        TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "
$$
InjectActivity")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(injectMethod.build())
                .build();
        String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
        return JavaFile.builder(packgeName, injectClass).build();
    }
    
}           

這裡就生成了一個 類名+

$$

的類,有一個方法叫inject參數是這個類本身,彈出一個toast。最後一個就是一個字元常量類。

public class TypeUtil {
    public static final String METHOD_NAME = "inject";
    public static final String ANNOTATION_PATH = "com.example.TestAnno";
}           

好了lib工程完畢。然後是主工程引用。

首先是在工程的gradle裡面配置下apt。

dependencies {
        classpath 'com.android.tools.build:gradle:2.3.2'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }           

然後在app的gradle裡面配置如下

apply plugin: 'com.neenbedankt.android-apt'
……
 compile project(':inject_annotation')
  apt project(':inject_comiler')           

這樣就行了。注意是apt 。為什麼要建立立javalib。因為javalib不能在引用adnroidlib,。而注解處理器是javalib來完成,app裡面,可以這引用這2個,apt如果換成complie 會提示錯誤,但不會影響啥。配置都好了,就是最後的使用了。

總結構

在我們MainActivity 上面加上注解,使用下。

@TestAnno
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectActivity.inject(this);//調用build生成的類
    }
}           

最後一個類就是我們的使用這個在運作時,調用生成build的類

public class InjectActivity {
    private static final ArrayMap<String, Object> injectMap = new ArrayMap<>();

    public static void inject(AppCompatActivity activity) {
        String className = activity.getClass().getName();
        try {
     
            Object inject = injectMap.get(className);

            if (inject == null) {
               //加載build生成的類
                Class<?> aClass = Class.forName(className + "
$$
InjectActivity");
                inject = aClass.newInstance();
                injectMap.put(className, inject);
            }
            //反射執行方法
            Method m1 = inject.getClass().getDeclaredMethod("inject", activity.getClass());
            m1.invoke(inject, activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           

這個類用了反射的方式查找了指定類執行了指定方法。有一個map來緩存,不用每次都重新反射。

當然你可可以不用反射,在建立build生成類的時候,實作一個接口,這裡直接強轉為接口,直接調用接口的方法也可以。這裡簡單一些用的反射

通過一個注解,就自動生成彈出toast的代碼看下自動 生成的代碼

public class MainActivity
$$
InjectActivity {
  public void inject(final MainActivity activity) {
    android.widget.Toast.makeText(activity, "from build",android.widget.Toast.LENGTH_SHORT).show();;
  }
}           

最後看下運作效果。

二、Android動态加載技術基礎之類加載(ClassLoader)

虛拟機類加載機制

  • 類加載過程是指虛拟機将描述類的資料從Class檔案中加載到記憶體,并對資料進行校驗,轉化解析和初始化,最終形成可以被虛拟機直接使用的Java類型的過程。
  • 在Java中,類的加載和連接配接過程都是在程式運作期間完成。雖然會增加運作時的性能開銷,但可以提高程式靈活性,這也是Java能夠實作動态加載的原因之一。

類加載的過程

虛拟機類加載過程分為加載,驗證,準備,解析,初始化,使用,解除安裝七個階段。其中驗證,準備,解析三個部分成為連接配接階段。

加載

一般來說,在遇到了以下四種情況的時候,虛拟機會開始進行類的加載:

  • 使用new關鍵字執行個體化對象,讀取或者設定一個類的靜态變量(被final修飾的除外,已經在編譯器被加入常量池),以及調用一個類的靜态方法的時候
  • 對類進行反射調用的時候
  • 當初始化一個類時,如果其父類沒有被加載,則先對其父類進行加載
  • 當虛拟機啟動的時候,使用者指定的(包含main方法)的類會被加載
    在類的加載階段,虛拟機會完成一下三件事情:           
  • 通過一個類的全限定名擷取定義類的二進制位元組流
  • 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構
  • 在Java堆中生成一個代表這個類的Class對象,作為方法區這些資料的通路入口。

驗證

這一階段是為了確定class檔案的位元組流包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。主要包括以下幾個過程:

  • 檔案格式驗證
  • 中繼資料驗證
  • 位元組碼驗證
  • 符号引用驗證

準備

準備階段是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些記憶體都将在方法區中進行配置設定。這個階段中進行記憶體配置設定的變量隻有被static修飾的變量,并将其設定為預設值,而真正的指派則在初始化階段。另外,被final static字段修飾的常量在編譯器就已經被指派。

解析

解析階段主要是虛拟機将常量池内的符号引用替換為直接引用的過程。

初始化

初始化階段是執行類構造器()方法的過程。

()與類的構造方法不同,()方法是由編譯器自動收集類中的所有類變量的指派動作和靜态語句塊中的語句合并而成的。編譯器收集的順序是按語句在源檔案中出現的順序決定的,靜态語句塊中隻能通路定義在它之前的靜态變量,定義在它之後的靜态變量,隻可以指派,不可以通路。

虛拟機會保證子類的()方法執行之前,其父類的()方法一定被執行(父類先與子類完成加載過程)

Java中的ClassLoader

類加載階段中,實作“通過一個類的全限定名擷取定義類的二進制位元組流”的動作被放在了虛拟機外部去實作,以便應用程式決定如何去加載所需要的類,實作這個動作的代碼子產品被稱為“類加載器”

從Java虛拟機的角度上講,隻存在兩種不同的類加載器:

  • Bootstrap ClassLoader:使用C++實作,是虛拟機的一部分。它主要負責加載存放在%JAVAHOME%/lib目錄中的,或者被-Xbootclasspath指定的類庫到虛拟機記憶體中,Bootstrap ClassLoader無法被java程式直接引用。
  • 繼承自java.lang.ClassLoader的類加載器:
  • Extension ClassLoader:主要負責加載%JAVAHOME%/lib/ext目錄中的,或者被java.ext.dirs系統變量指定路徑的所有類。
  • Application ClassLoader:也被稱為系統類加載器(因為其實getSystemClassLoader的傳回對象),主要負責加載使用者類路徑(ClassPath)下的類庫

類的雙親委派模型

這些類加載器之間的關系如下:

雙親委派模型中,除了頂層的BootstrapClassLoader,其他類加載器都要求有自己的父類加載器,這裡的類加載器一般以組合的方式實作。

  • 雙親委派模型的工作過程是:當一個類加載器收到一個類加載請求的時候,他首先不會自己加載這個類,而是将這個請求委派給父類加載器去完成,隻有當父類加載器無法完成這個加載請求時,子加載器才會嘗試自己去加載。
  • 雙親委派模型的作用:
  • 使得java類随着它的類加載器一起具備了一種帶有優先級的層次關系
  • 保證Java環境的穩定性
  • 避免重複加載,如果已經加載過一次Class,就不需要再次加載,而是先從緩存中直接讀取。

Android中的ClassLoader

  • 由于Android虛拟機的執行檔案是Dex檔案,而不是JVM中的Class檔案,是以Java中的類加載器是無法加載Dex檔案的,是以,Android中存在另外一套ClassLoader。

Android的ClassLoader類型

Android中的ClassLoader根據用途可分為一下幾種:

  • BootClassLoader:主要用于加載系統的類,包括java和android系統的類庫,和JVM中不同,BootClassLoader是ClassLoader内部類,是由Java實作的,它也是所有系統ClassLoader的父ClassLoader
  • PathClassLoader:用于加載Android系統類和開發編寫應用的類,隻能加載已經安裝應用的 dex 或 apk 檔案,也是getSystemClassLoader的傳回對象
  • DexClassLoader:可以用于加載任意路徑的zip,jar或者apk檔案,也是進行安卓動态加載的基礎

Android中ClassLoader的繼承關系

ClassLoader的執行過程

  • 從上面的ClasLoader結構圖可以看到,ClassLoader的主要邏輯主要集中在ClassLoader和BaseDexClassLoder這兩個類中。

ClasLoader

  • ClasLoader是所有ClassLoader的父類,它定義了加載Class的一般行為。
  • 與Java中不同,Android中加載類的過程主要是由loadClass方法實作,而在Java中則是findClass方法。
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }
        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }
    return clazz;
}
           
  • 可以看到,當收到一個加載類請求的時候,ClassLoader會先調用findLoadedClass查詢是否本類是否被本加載器加載過(調用JNI方法),如果沒有被加載則把請求委托給父加載器,當父加載器無法完成加載行為的時候,才會調用findClass方法嘗試自己加載,而ClassLoader中的findClass方法并沒有實作,而是交給子類去是實作。

BaseDexClassLoader

  • 作為ClassLoader的直接子類,BaseDexClassLoader實作加載findClass方法的主要邏輯,而其子類DexClassLoader和PathClassLoader都隻是加載路徑以及某些行為不同而已
  • 可以看到findClass方法是又是調用了pathList的findClass方法去加載類,而pathList則是一個DexPathList對象,它的findClass對象是這樣實作的:
  • 其中DexFile是Dex檔案在Java中的表現形式,而它的loadClassBinaryName方法則是最後調用了JNI方法去完成在dex檔案在加載Class對象。

DexClassLoader和PathClassLoader

  • DexClassLoader和PathClassLoader都是BaseDexClassLoader的子類,他們的實作也很簡單,隻是構造方法傳入了不同的參數而已:
  • DexClassLoader :
  • PathClassLoader:
  • 可以看到,DexClassLoader和PathClassLoader的差別就是,PathClassLoader的第二個參數傳為NULL,回到BaseDexClassLoader中可以看到:
  • 根據注釋可以看到optimizedDirectory參數是用來放置DexFile的,那麼具體是怎麼回事呢,再進入DexPathList
  • optimizedDirectory被傳進了makeDexElements方法
  • 又被傳進了loadDexFile
    ![image](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy84MTQ5OTY5LTRlODA0Mjk4YzlkMzU2NTgucG5nP2ltYWdlTW9ncjIvYXV0by1vcmllbnQvc3RyaXB8aW1hZ2VWaWV3Mi8yL3cvNjE3L2Zvcm1hdC93ZWJw?x-oss-process=image/format,png)
               
  • 可以看到,如果optimizedDirectory為NULL,則會以原來的路徑建立DexFile,否則會以optimizedDirectory為路徑建立DexFile
  • 其實optimizedDirectory是要求一個内部路徑的,因為動态加載去加載的可執行檔案一定要存放在内部存儲。而DexClassLoader可以指定optimizedDirectory,是以它可以加載外部的dex,并且這個dex會被複制到内部路徑的optimizedDirectory;而PathClassLoader沒有optimizedDirectory,是以它隻能加載内部路徑的dex,也就是存在于已經安裝過的apk裡面的。

參考

https://www.jianshu.com/p/144df8826d15 https://blog.csdn.net/spinchao/article/details/72972185

最後

我們今年整理了一份阿裡P7級别的Android架構師全套學習資料,特别适合有3-5年以上經驗的小夥伴深入學習提升。

主要包括騰訊,以及位元組跳動,華為,小米,等一線網際網路公司主流架構技術。如果你有需要,盡管拿走好了。

以下為我們整理的資料免費分享;【阿裡P7】Android進階教程+BAT面試題

1.Android進階技術腦圖

2.P7級Android進階架構視訊教程

3.Android核心進階技術PDF文檔+BAT大廠面試真題解析

4.Android思維腦圖(技能樹)

1.Android進階技術腦圖;

查漏補缺,體系化深入學習提升

2.【Android進階架構視訊教程】;

全套部分展示;

java與Android核心進階專題視訊與源碼

阿裡P7級全套進階學習視訊;

3.Android核心進階技術PDF文檔,BAT大廠面試真題解析

免費分享

為什麼免費分享?

我的目的是讓更多需要的Android開發朋友能夠提升自己的技術水準

無論是Android,還是qq,微信,360等,想在網際網路上最大程度推廣,就必須免費!

如果我的學習資料對你有幫助,點個贊,謝謝!

繼續閱讀