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 @com.foundation.KeepProguard class * {*;} 2. 不混淆有方法或成员被注解的类 keepclassmembers class * {@com.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的方式,让输入方指定文本标签。输出方解析标签后,根据约定的标签含义进行组装,可参考com.android.map.MapCardMarkerView#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日记(二)

posted @ 2020-04-28 13:07  是个写代码的  阅读(885)  评论(2编辑  收藏  举报