android日記(一)


1.關於Java版本

  • Java8 = Java SE 8.0 = JDK1.8
  • java14都出來了,然后java8(2014年發布)仍是主流。
  • java8的接口中是可以有實體方法的,不過要是靜態方法。

2.什么是類名.class

  • 使用類名.class,得到的是是類加載信息Class<?>,它不同於Object,其下主要有fromName()、getClassLoader()、getName() 、isAnnotationPresent()、getAnnotation()等方法。
  • 使用時,如果類還沒被加載過,會將其加載至內存(方法區),包括類信息、靜態成員/方法。
  • 當執行new創建類對象時,才會加載非靜態成員方法只堆內存中。

 3.Android混淆

  • 混淆為了保護代碼在被反編譯成源碼時不容易識別,從而提高安全性
  •  在build.gradle配置中設置minifyEnable = true即開啟混淆,通常對buildTypes下的debug不開啟,release開啟
  • 配置混淆文件,build.gradle中設置proguardFiles = getDefaultProguardFile(“proguard-android.txt”), “proguard-rules.pro”。其中proguard-android.txt是sdk默認的混淆配置文件,在每個模塊下都有一個proguard-rules.pro文件,可供用戶配置當前模塊的混淆規則
  • 哪些需要保留不被混淆:使用了反射的類、需要Jni訪問的方法、在Manifest.xml注冊過的四大組件、在xml中使用的自定義View、fragment也可能在xml中使用、資源id的R文件、js調用的方法、枚舉類、序列化的數據類Serializable和Parcelabe、數據實體類等
  • 關鍵字,keep保留指定類和類成員不被混淆,keepclassmembers只保留類的成員不被混淆和移除

4.自定義注解@interface用於混淆

  • 注解可以理解成一種特殊的接口,應用了注解的類就相當於是實現了這個特殊的接口
  • 定義注解時,可以用元注解對注解進行注解,@Retention(RetentionPolicy.RUNTIME)用於指定注解的生命周期,包括RetentionPolicy.SOURCE < RetentionPolicy.CLASS < RetentionPolicy.RUNTIME;而@Target({ElementType.METHOD})用來指定注解的可以作用的對象,包括TYPE【接口、類、枚舉】、METHOD【方法】、FIELD【字段】等
  • 為注解添加屬性,例如:1.定義注解和屬性 public @interface MyAnnotation { String color();} 2.使用注解和屬性 @MyAnntion(color = “you color value”) 3.獲取注解和屬性String color = getClass().getAnnotation(MyAnnotation.class).color();
  • kotlin中定義注解,annotation class MyAnnotation(val color : String)
  • 使用自定義注解來靈活配置混淆規則,1.不混淆被注解了的類 keep @ctrip.foundation.KeepProguard class * {*;} 2. 不混淆有方法或成員被注解的類 keepclassmembers class * {@ctrip.foundation.KeepProguard *; }
  • 參考:https://blog.csdn.net/pang9998/article/details/83505980?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-6&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-6

5.理解kotlin注解@JvmStatic和@JvmFiled

  • 可以把kotlin代碼反編譯成java代碼,分析會更透徹
  • 反編譯的方法:在當前kotlin代碼上——AndroidStudio Tools——Kotlin——show kotlin bytecode——compile
  • @JvmStatic的作用:反編譯后的java代碼中發現,compaion object{}閉包中申明的靜態方法,在java代碼中實際是新建了靜態內部類Compain,然后在Compain定義了那個靜態方法。當@JvmStatic注解后,在java代碼中調用該靜態方法,不用顯示的寫Compain類。
  • @JvmFiled的作用:不加@JvmFiled默認就會生成setter()和getter()方法,賦/取值操作實際上是調用setter()和getter(),而變量本身對java代碼是不可見的,可以認為是private。使用@JvmField標記后,反編譯后的java代碼中發現,不會再自動生成對應的setter()和getter()方法,此時變量對java代碼也是可見的。
  • ARouter對kotlin中的@Autowired注解賦值,為什么需要對變量用@JvmFiled標記:查看源碼com.alibaba.android.arouter.core.AutowiredServiceImpl#autowire()方法中,通過反射獲取變量名並進行賦值,但是反射時並沒有設置setAccessible(true)【這句話的作用是允許訪問私有變量】,不能訪問私有變量,從而導致賦值失敗。
  • 如果面試時問kotlin語法相關的知識,不管知道還是不知道,都可以首先回答反編譯成java代碼進行分析
  • Kotlin KDoc文檔中的塊標簽,與java的JDoc類似,主要有@param、@author、@return、@see等。這些不是注解,而是塊標簽。

 6.Android富文本有什么騷操作

  • 最常規的方法是通過SpannableString,譬如ForeGroundColorSpan、ClickSpan、UnderlineSpan、DynaminDrawableSpan等。
  • 還可以使用Html實現,Html.fromHtml("<font color = '#fb5112'>10</font>公里,大約<font color = '#fb5112'>3</font>分鍾") ,其中fromHtml返回的是一個Spanned對象。
  • 騷操作:輸入方在string文本中,人為地插入一些自定義標簽;輸出方對文本中的標簽進行解析,並根據約定的標簽含義對文本進行多樣化處理,即轉成SpannbleString。
  • 尤其適用於在定義公共UI組件時,顯示需要有富文本樣式,但是又不想讓輸入方隨意輸入自己的SpannableString,因為想對UI樣式類型收口,比如可以在3種顏色,4種字體組合樣式。
  • 這時常規做法是弄一個styleModel,並定義字段text、color、type等,然后讓輸入方把整個model傳過來做組裝。
  • 但如果想簡化,工具方法只想接受輸入方的string字段,不想搞這個model。完全可以采取自定義標簽的方法,模仿Html的方式,讓輸入方指定文本標簽。輸出方解析標簽后,根據約定的標簽含義進行組裝,可參考ctrip.android.map.CtripMapCardMarkerView#parseHighlightText方法中,定義了</color>,</highlight>,</¥>等標簽;例如,輸入方傳入”<color:#fb5112>${distanceKilometer}</color>公里,大約<color:#fb5112>${durationMin}</color>分鍾”文本,會在方法內部組裝相應字體顏色的富文本。

7.bindView新特性

  • 不想寫findViewById(),所以有了ButterKnife框架。
  • ButterKnife的原理在於手動寫@BindView注解,然后在編譯期間,會根據注解生成器,生成“類名_ViewBinding.java”,在這個自動生成的類中完成了view的綁定。
  • 使用kotlin,引入Kotlin Android Extension后,直接在kotlin文件中使用id作為view的實例使用。
  • kotlin兩種導包的區別,轉java字節碼后發現,藍框中實際是直接findViewById()綁定view。而紅框則有玄機,使用的是findCachedViewById(),維護了一個緩存map,只有map中沒有對應的值時候,才執行findViewById(),並把綁定的view存到map中。

  • kotlin這樣干其實有個問題,當緩存表中沒有view時,調用this.getView去拿rootView,由於fragment的onCreateView()方法返回前,rootView其實還為空【查看findCachedViewById()源碼,發現該方法中不會執行findViewById綁定view】,導致此時使用view會有空指針的問題。

  • 將AndroidStudio升級至3.6+,gradle插件升級3.6+(對應的gradle version 5.4.1-5.6.4), Android Gradle Plugin version 與Gradle Version的對應關系可以看官網https://developer.android.com/studio/releases/gradle-plugin?hl=zh-cn ,Android提供了viewBinding新特性,在gradle中配置viewBinding.enable=true。編譯時會自動生成以(類名+Bind.java)類,類中已完成view綁定,且不存在上述kotlin空指針的問題。
  • 使用ActivityViewBindingBinding binding = ActivityViewBindingBinding.inflate(getLayoutInflater()); binding.view1.setText("通過設置viewBinding.enable = true");

 8.關於Activity重建

  • 導致Activity重建的場景:activity在后台因為內存不足被回收;屏幕方向或者語言等系統配置發生改變;開啟不保留活動離開頁面后又返回。
  • activity重建時,生命周期onCreate(Bundle saveInstanceState)中傳入的saveInstanceState值不為空,該值由AMS在重啟activity時傳入。
  • Activity 在非正常銷毀時,會在執行onStop()前,執行 onSaveInstanceState ()方法,該方法中完成自動收集 View Hierachy 中每一個 “實現了狀態保存和恢復方法” 的 View 的狀態,這些狀態數據會在 onRestoreInstanceState ()時回傳給每一個 View。並且回傳時是依據 View 的 id 來逐一匹配的。
  • 有時候需要自己額外存一些狀態,比如activity中定義的實例,那就需要自己在onSaveInstanceState()方法中進行存入操作,然后取出操作在onRestoreInstanceState()中取出。
  • 重建時,還是要經過一個完整的performLaunchActivity流程【AMS還是通知ActivityThread調用onCreate()在super.onCreate()中創建window,並在setContentLayout()后將布局添加到window】。只是在重建生命周期onStart()后,會回調onRestoreInstanceState()方法,在這個方法中,將保存的view狀態取出並完成View賦值恢復視圖狀態。
     private Activity performLaunchActivity(Activi
             
             ...
              mInstrumentation.callActivityOnCreate(activity, r.state);
                   
                   r.activity = activity;
                   r.stopped = true;
                   if (!r.activity.mFinished) {
                       activity.performStart();
                       r.stopped = false;
                   }
                   if (!r.activity.mFinished) {
                       if (r.state != null) {//只有當保存的狀態不為空時,AMS才會回調onRestoreInstanceState() mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); }
                   }
                   if (!r.activity.mFinished) {
                       activity.mCalled = false;
                       mInstrumentation.callActivityOnPostCreate(activity, r.state);
                
                   }
         }

9.關於App異常重啟

  • 有些情況下,app進程也會重啟,比如app長時間在后台、app權限丟失(可以在開啟app的情況下,到設置中關閉某個權限),這時不僅activity被回收,app進程也一並回收了。再次進入app時,系統會重建Application和Activity。
  • Application被異常殺死時,會把activity界面現場保存,並在Application重啟時,將保存的狀態傳給ActivityThread,然后也是走launcher流程(AMS->performLaunchActivity())傳給對應的Activity。
  • 只不過在makeApplication之后,不是直接去啟動MainActivity,而是啟動被銷毀前的Activity。
  • 這時問題就來了:App在銷毀前保存的單例數據(靜態)會隨着app的銷毀一同銷毀,而在App重建時,只會恢復Activity的現場狀態,卻不會恢復靜態數據。如果這時恢復出來的Activity中,恰好需要用到某個靜態數據,就出現拿不到數據的情況,從而出現各種詭異的異常。
  • 舉個栗子:在銷毀前當前Activity是一個登錄頁LoginActivity,在登錄頁中發起登錄請求的URL保存在一個靜態變量中,這個變量是由發起登錄方,在跳轉到登錄頁時動態設置的。正常情況下,進入到登錄頁后,靜態變量中的url都有值的,不影響登錄。而當app異常重啟后,會直接恢復出當前的LoginActivity。然而, 這時靜態URL變量因為缺乏賦值過程而為空。
     
               
    public class Instance {
    public static String loginUrl;//保存登錄url的靜態變量
    }

    rootView.setOnClickListener {
    //跳轉至登錄頁時,對靜態變量賦值 Instance.loginUrl
    = "https://xxx/xx/x" startActivity(Intent(this, LoginActivity::class.java)) }
  • 因此,不要總想着通過static偷懶省事,指不定哪天就給爆個大坑出來。如果代碼中有存取靜態數據的場景中,在App重建時,應該考慮如何恢復靜態變量數據。比如例子中,可以把loginUrl,放在Intent中攜帶到LoginActivity。

10.如何科學添加fragment

  • Fragment、FragmentActivity、Activity:Fragment自Android4.0引入,設計之初為了解決android平板碎片化問題;FragmentActivity是為了兼容4.0以下也能用Fragment,其中提供了操作了Fragment的一些方法,比如dispatchFragmentsOnCreateView();在4.0以后,FragmentActivity與Activity無異。
  • add與replace: 往同一id的container中添加多個fragment,使用add不會清除container已經存在的fragment,而使用replace會清除,只保留當前fragment。當通過add操作重復添加同一fragment實例還會拋異常“Exception: Fragment already added”, 而replace不會。
  • show與hide:fragmentManager.beginTransaction.show(fragment)/hide(fragment),可對fragment動態顯示或隱藏。在fragment中,也可以通過getView.getParenet()來設置container的visibility屬性,實現動態顯示和隱藏。
    private fun showParent() {
        (view?.parent as ViewGroup?)?.visibility = View.VISIBLE
    }
  • fragment返回棧,addToBackStack()將當前事物添加到返回棧中,按返回時界面回歸到當前事物狀態。也可以監聽返回onKey,手動remove()掉fragment。
  • commit與commitAllowLossState: look the fuck source to see,他兩的區別在於使用commit操作時,會進行checkStateLoss()檢查狀態,如果此時狀態已經被保存(activity執行過onSaveInstanceState()方法),那么就會拋異常“Exception: can not perform this action after onSaveInstanceState”。而commitAllowLossState()方法不會執行這段檢查,不管狀態有沒有被保存,都可以進行commit。
    private void checkStateLoss() {
        if (isStateSaved()) {
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
        if (mNoTransactionsBecause != null) {
            throw new IllegalStateException(
                    "Can not perform this action inside of " + mNoTransactionsBecause);
        }
    }
  • 關於fragment構造函數:通過下面的構造函數來創建TestFragment,會不會有問題呢?答案:一般情況下,也沒什么問題。但是當fragment重建時會崩潰,報異常“Exception: Could not find Fragment constructor”。原因在於,在fragment所依附的activity發生重建時,會調用fragment的默認構造函數(無參),執行new TestFragment()創建新實例,並取出保存的狀態初始化實例,這一過程從現象上稱為fragment restoreAllState。如果這時,Activity#onCreate()重建過程中,找不到fragment的無參默認構造函數,就會出現上面的異常。【Tips: 面試官問遇到過什么問題時,可以拿出來說一說
    class TestFragment(private var type: String) : Fragment() {}
  • 如何添加fragment:如果直接按下面的實現,創建一個新的fragment實例,然后添加的方式,正常情況下沒什么問題。問題還是出在頁面重建的時候,super.onCreate(saveInstanceState)會自動恢復先前已經添加的fragment。之后,又重新創建了一個新的fragment實例,覆蓋到先前的viewContainer中,即新創建一個fragment覆蓋掉了系統自動恢復出來的fragment。這樣會導致,新創建出的fragment沒法恢復先前的狀態,試想一下,如果fragment中放着一個recyclerView,銷毀前已被滑動到某個位置,重建后,recyclerView被拉回到頂部,用戶肯定不高興的。
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)//
        //頁面重建時會創建新的fragment實例,無法恢復先前的狀態
        supportFragmentManager.beginTransaction()
            .add(R.id.rootView, TestFragment())
            .commit()
    }

因此,在添加fragment前,先看viewContainer是否已經存在恢復出來的fragment實例。只當不存在時,才創建新的實例。重建時,已恢復出實例,便不再創建。

if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
    //先看viewContainer中是否存在fragment
    supportFragmentManager.beginTransaction()
        .add(R.id.rootView, TestFragment())
        .commit()
}
  • fragment傳值的錯誤姿勢1——構造函數:上面已經分析到,直接通過構造函數傳參會有crash風險,即使要這樣干,那也一定要手動給fragment寫明無參的默認構造函數。然后,即便加了默認構造函數,還是存在問題:重建出來的fragment拿到的參數為null(通過默認構造函數新創建的Fragment實例,壓根就沒傳參數過來),頁面缺少了外部傳參,顯示異常。
  • fragment傳值的錯誤姿勢2——Arguments:通過fragment.setArguments(Bundle bundle),將外部參數放置在arguments。與構造函數傳參不同,arguments的狀態會在異常銷毀時被存儲,並在fragment restoreAllState時取出,因為不會出現丟失數據的情況。Arguments傳值基於Binder機制,有大小限制(不同版本限制大小不同,通常1MB),一旦傳遞的數據大小超過限制,就會報TransactionTooLargeException異常 。因此除非明確知道數據大小沒有超出限制風險,否則不建議通過arguments傳值。
    @JvmStatic
    fun newInstance(type: String): TestFragment {
        return TestFragment().apply {
            arguments = Bundle().apply { putString("TYPE", type) }
        }
    }
  • fragment傳值的正確姿勢——實例方法賦值,添加fragment
    private String type;
    private void setType(String type) {
        this.type = type;
    }
  • 總結:為了保證頁面重建時能夠正常恢復fragment狀態,並解除傳值大小限制,一個科學的fragment添加方式如下:
    private fun addTestFragment() {
        //先看viewContainer中是否存在fragment
        val fragment =
            if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
                supportFragmentManager.findFragmentById(R.id.fragmentContainer) as TestFragment4
            } else {
                TestFragment4()
            }.apply { 
                //實例值傳遞
                setType("科學添加fragment")
            }
        supportFragmentManager.beginTransaction()
            .replace(R.id.rootView, fragment)
            .commitAllowingStateLoss()
    }

下一篇:android日記(二)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM