android日记(二)
上一篇:android日记(一)
1.在AndroidStudio中查看源码对应的字节码
- .java文件:build后,会在app/build/intermediates/javac/..目录下,生成每个.java文件编译生成的.class字节码。
- .kt文件:kotlin代码上——AndroidStudio Tools——Kotlin——show kotlin bytecode——compile,即可看到对应的java字节码.class。
2.for循环标签label
- 可以给每个for循环取个名字,叫做标签
- 在多层循环中,使用continue、break时,可以带上标签,实现多层跳转
- 举个例子:
//test circle int j = 0; outer : for(int i = 0;i<10;i++){ System.out.println("outer : i = " + i); inner : for(;j<10;j++){ System.out.println("inner : j = " + j); if(j == 4){ System.out.println("continue inner:"); continue inner; } if(j == 5){ System.out.println("break inner:"); j ++;// have to increase break inner; } if(j == 6){ System.out.println("continue outer:"); j ++;// have to increase continue outer; } if(j == 7){ System.out.println("break outer:"); break outer; } } }
3.使用Space控件设置间距
- Space是一款轻量级空间,常用于设置组件间的间隙。
- 其源码显示,其draw()方法为空,减少了渲染绘制的过程。
- 举个例子,content组件上下的间距可能会动态变化,就可以弄个LinearLayout,在contentView上下分别设置一个Space,通过设置Space的高度变化来动态上下间距。
4.Okhttp遇到的坑-Unexpected char 0x7231 at ** in xx value
- 使用Okhttp框架进行网络请求时,常常需要在request或者header中添加一些设备信息字段,比如在header中设置(由各种设备信息拼接组成的)user-agent信息。
public static Headers createHeaders(String url) { StringBuilder ua = new StringBuilder(); ua.append(DeviceUtils.getBrand() + "|") .append(android.os.Build.DEVICE + "|") .append(android.os.Build.MANUFACTURER + "|") .append(android.os.Build.PRODUCT); okhttp3.Headers.Builder headersbuilder = new okhttp3.Headers.Builder(); headersbuilder.add("User-Agent", ua.toString()); return headersbuilder.build(); }
- 上面的代码一般情况下也没什么问题,但有些时候会出现异常导致崩溃。分析异常信息:在字段User-Agent字符串的第40个位置出现了不可解析的字符。
Unexpected char 0x7ea2 at 40 in User-Angent value: osversion:8.1.0|MODEL:20190325d|PRODUCT:红辣椒8x|BRAND:xiaolajiao|qemu:
- 到这里就只能去查看OKhttp源码,看看它的字符校验策略,定位到okhttp3.Headers.checkValue()。下面标红处清楚的显示,字符只能是ASCII码,否则就抛了异常,Exception信息与开头遭遇的崩溃日志吻合。
static void checkValue(String value, String name) { if (value == null) throw new NullPointerException("value for name " + name + " == null"); for (int i = 0, length = value.length(); i < length; i++) { char c = value.charAt(i);
//字符校验逻辑 if ((c <= '\u001f' && c != '\t') || c >= '\u007f') { throw new IllegalArgumentException(Util.format( "Unexpected char %#04x at %d in %s value: %s", (int) c, i, name, value)); } } } - 谁的锅?崩溃的原因是传入的字符User-Agent中的字符含有中文。某些国产手机就是这么“牛”,把设备信息改成中文了。
- 怎么修?把拿到的设备信息,先转成ACSII码,再丢给User-Agent。
private static String getUserAgent(@NonNull String userAgent) { StringBuffer sb = new StringBuffer(); for (int i = 0, length = userAgent.length(); i < length; i++) { char c = userAgent.charAt(i); if (c <= '\u001f' || c >= '\u007f') {
//convert non-acsii to acsii sb.append(String.format("\\u%04x", (int) c)); } else { sb.append(c); } } return sb.toString(); }
5.Interge的缓存策略
- Integer类的装箱与拆箱
List<Integer> list = new ArrayList<>(); list.add(1);//装箱 int firstElement = list.get(0);//拆箱
当Integer与int进行比较时,会把Integer自动拆箱成int
int i1 = 10; Integer i2 = 10; Integer i3 = new Integer(10); boolean b1 = i1 == i2;//true, 自动拆箱 boolean b2 = i1 == i3;//true, 自动拆箱
- 看下面的代码,b1=false是比较两个不同的Integer对象的结果很好理解。那b2为什么也为false呢?更奇怪的是b3怎么就为true呢?java在编译Integer i3 = 127时,会翻译成Integer i3 = Integer.valueOf(),而玄机就在valueOf()方法体内。
Integer i1 = new Integer(127); Integer i2 = new Integer(127); Integer i3 = 127;
Integer i4 = 127;
Integer i4 = 127;
boolean b1 = i1 == i2;//false, 两个不同的对象
boolean b2 = i1 == i3;//false, 也是两个不同的对象
boolean b3 = i3 == i4;//ture, valueOf()内部的IntegerCache[]的缓存策略在valueOf()方法体内,出现了一个名叫IntegerCache的数组,当传入valueOf()的参数,在IntegerCache.low-IntegerCache.high之间时,结果从IntegerCache[]数组中返回,否则才创建新的Integer对象。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
-
进一步查看IntegerCache的定义,IntegerCacher将[-128, 127]的值全已经new Integer()后存入数组里缓存起来了,也就是说,任何时候使用Integer i = 127 或者 Integer i = Integer.valueOf(127)时,都不会再创建新的对象,而是从IntegerCache[]中取缓存对象。所以,Integer i3 = 127 ,与Interge i1 = new Integer(127)实际是两个不同的对象,而与Integer i4 =127实际都是从IntgerCache中取出的同一个对象。
private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { int h = 127; ... high = h; cache = new Integer[(high - low) + 1]; int j = low; //range [-128, 127] for (int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); ... } }
- 在看下面的代码,由于128不在IntegerCache的缓存范围内,每次用到的时候,都会重新new Integer(128)创建新对象。
Integer i1 = 128; Integer i2 = 128; Integer i3 = Integer.valueOf(128); boolean b1 = i1 == i2;//false,128不在缓存范围[-128,127]内 boolean b2 = i1 == i3;//false,128不在缓存范围[-128,127]内 boolean b3 = i3 == i3;//false,128不在缓存范围[-128,127]内
6.Java浮点数的精度问题
- 先看代码,再来问题。都是比较“0.1==0.1”,但是b1=true、b2=false、b3=false。
- 一切还得从浮点数的二进制表示说起,浮点数转换为二进制表示的规则如下:
- 整数部分乘2取余后逆向排序
- 小数部分乘以2取整数部分,直到小数部分为0为止,然后顺序排序
例如:10进制0.09375转为二进制的过程如下,
0.09375 * 2 = 0.1875 取 0
0.1875 * 2 = 0.375 取 0
0.375 * 2 = 0.75 取 0
0.75 * 2 = 1.5 取 1
0.5 * 2 = 1.0 取 1
0.0 * 2 = 0
Thus, 0.09375 = 00011
例如:正如十进制因为无限循环无法精确表达1/3一样,二进制也存在无法精确表示浮点数的情况,比如0.1,也是个无限循环的
0.1 * 2 = 0.2 取 0
0.2* 2 = 0.4 取 0
0.4 * 2 = 0.8 取 0
0.8 * 2 = 1.6 取 1
0.6 * 2 = 1.2 取 1
0.2 * 2 = 0.4 取 0
Thus, 0.1 = 0 0011 0011 0011 ......
- 再来说说类型升级,如果运算符两边的数值类型不同,需要进行类型升级,升级的规则 int < long < float < double (double类型级别最高)。
- 现在可解答为什么 0.1f == 0.1d 是false了,比较时,会把float型的0.1f升级成double型,再与0.1d比较,然而由于float位数比double少,float升级成double的时候,是直接补0的,自然就与原本的0.1d不一样了。
- 进而上述提到的float数通过“==”相互进行比较的问题,也是同样的,计算机在把每个浮点数转为二进制过程中,绝大多数都会丢失精度,从而出现false自然而然呢。所以,写代码时严令禁止通过“==”和“!=”来比较浮点数。不过精度问题不会大于>小于<符号的结果。
- 那有什么办法来判断两个浮点数是否相等呢?在对精度要求不高的场景中,可以计算两者的差值,如果差值在一定范围内,就认为两者相等,
- 如果是对精度要求非常高的场景,还可以使用BigDecimal类。注意构造BigDecimal对象是需要传入字符串,而不是浮点数。不过BigDecimal有严重的性能问题,需要慎用。
- 那BigDecimal是啥原理呢?基本上,就是把传入的字符串浮点数,去小数点,然后解析Long.valueOf()解析成一个long(内部成员变量名smallValue),最后加减乘除实际都是对其成员smallValue的操作。
7.使用git rebase合并多个commit
- 完成某个业务功能的提交,免不了进行了多次commit提交。当这些记录都出现commit log中时,看上去不够优雅。
- 使用git rebase,可以将多个commit合并成一个。前提:在commit被push到远程仓库之前进行。
- 比如本地已经进行了git commit -m "update_1"、git commit -m "update_2"、git commit -m "update_3后。
- 可以执行git rebase -i HEAD~3,把最近3次commit合并成一个commit,也可以通过git log查看commit id后,指定git rebase -i start_id end_id,也可省略end_id就代表从start_id到当最新一次
- 并在vi命令中(esc + i)进入/退出vi编辑环境,配置commit合并规则 & 修改合并后的commit -m "注释"。
- 常用vi退出保存命令,:w —— 保存不退出、:wq —— 保存并退出、:wq! —— 强制保存并退出、:q! —— 不保存强制退出、:e! —— 放弃所有修改,从上次保存文件开始再编辑。
- B、C、D合并成B'后,git log不会再出现B、C、D,只会出现B'。
- 在merge分支时,可以使用git rebase变基,比如从master上拉出新分支feature,当master上有新的改动后,可以通过featrue: git merge master来同步master的改动。不过merge会让master的变更记录也出现在feature分支上。
- 如果想保持feature分支上干净,只有自己的commit记录,那就可以使用feature: git rebase master,完成变基。
- 过程中任何时候都可以执行git rebase --abort放弃操作,并回到操作前的状态。
8.泛型
- 一句话解释范型:泛型就是在定义一个类、接口或方法时,不指定形参的具体类型,而是使用参数化类型(将具体类型参数化),用于匹配(调用方传入的)不同的实参类型,从而避免重复定义(的一种机制)。
- 举个栗子:ArrayList list = new AarryList()如果没有泛型设计,就将在定义中出现各种类型的ArrayList,诸如IntegerArrayList、StringArrayList、BooleanArrayList等等,那简直不要太糟糕了。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
- 和多态相比:多态使用Object类型实现参数的“任意化”,这种“任意化”带来的缺点是要做显示的强制类型转换,转换要求开发者对实际参数类型可以预知,否则将出现安全问题。而泛型,不用显示地强转类型,能够在编译期间检查类型安全。
- 擦除机制:泛型只会作用于编译期间,编译之后,所有泛型信息都会被擦除,也就是说在.class字节码中,是不存在泛型的。例如,List<Integer>和List<String>的getClass()结果都是List,他们在编译后,JVM看到的只有List,没有泛型参数。这也是为什么,反射(在运行时生效)可以做到类型擦除的原因,比如对List<Integer>可以通过反射执行add("string")而不报错的原因。
- 范型类:泛型标识符可以随便取,常用的T、K、V、E等等都可以。
public class Generic<A> { private A var; //构造函数,实参类型由外部指定 public Generic(A var) { this.var = var; } //普通方法,返回类型由外部指定 private A getVar() { return var; } }
- 泛型接口:与泛型类的定义类似,在实例化泛型时,传入的具体类型必须时类类型,而不能是简单类型。
//泛型接口 public interface GenericInterface<B,C> { //接口中的方法,入参、回参类型均由外部指定 public B fun(C param); }
- 泛型方法:用标识符<T>放在返回类型的前面,定义方法为泛型方法,定义时可以指定多个泛型参数。
public class MyGenericTest { //泛型方法 private <T> void handleSomeThing(T t) { //handle your logic } //泛型方法 public <T, E> void invoke(E e, T t) { handleSomeThing(e); handleSomeThing(t); } }
- 实例化泛型参数:在泛型方法中,如果创建获取泛型参数类的实例,不支持直接new T(),需要在定义方指定Class<T>参数,然后通过反射newInstance()来创建对象。然后,调用发在调用时传入具体类型的class。
public class MyGenericTest { // public <T> T create() { // return new T();//报错,不支持直接new T()生成实例 // } public <T> T create(Class<T> clazz) throws InstantiationException, IllegalAccessException { //通过反射来创建泛型参数的实例 T instance = clazz.newInstance(); return instance; } private void operate() { MyGenericTest test = new MyGenericTest(); try { String result = test.create(String.class);//调用方传入具体类型class } catch (Exception e) { e.printStackTrace(); } } }
- 泛型通配:通配符号用?来表示,代表不确定的类型。与泛型T的区别在于,T表示一个确定的类型,可以用于类型检查。而?表示的是不确定的类型。
List<? extends Animal> listAnimals
因此,
// 可以 T t = operate(); // 不可以 ? car = operate();
可以使用<? extends T>和<? super T>约束通配上下边界。至于通配符的应用场景,看下面的例子。
static int countLegs (List<? extends Animal > animals ) { int retVal = 0; for ( Animal animal : animals ) { retVal += animal.countLegs(); } return retVal; } static int countLegs1 (List< Animal > animals ){ int retVal = 0; for ( Animal animal : animals ) { retVal += animal.countLegs(); } return retVal; } public static void main(String[] args) { List<Dog> dogs = new ArrayList<>(); // 不会报错 countLegs( dogs ); // 报错 countLegs1(dogs); }
- Class<T>与Class<?>:前面已经提到在泛型中需要创建泛型参数类的实例时,可以传入Class<T>。同样的Class<?>也可以在类型不明确的情况下,通配类型用于反射构造实例。不过Class<T>需要在泛型类或者泛型方法中使用,而Class<?>可以不在泛型类和泛型方法中使用。
// 可以 public Class<?> clazz; // 不可以,因为 T 需要指定类型 public Class<T> clazzT;
- 特殊情况:泛型类中的泛型方法、静态泛型方法,其泛型类型T与类中的泛型类型T是同步的,相互独立。
9.使用反射
- 反射是在运行时,能够动态获取和调用一个类的所有属性、方法,的一种机制。
- 反射获取类信息,使用Class.forName()方法,需要捕获ClassNotFoundException。Throwable家族有Error【StackOverFlow、OutOfMemory】和Exception两大分支,Exception又氛围RunTimeException【NullPointer、IIegalArgument、ArrayIndexOutOfBound等】和非运行时异常(在编译检查时)【ClassNotFound,Interrupter,IOException等】。
try { Class clazz = Class.forName("com.example.fragmenttest.TestClass"); testReflection(clazz); } catch (ClassNotFoundException e) { e.printStackTrace(); }
- 反射获取构造函数,其中getConstructors()只获取public构造函数,getDeclaredConstructors()则获取所有的构造函数。
//获取public构造函数 Constructor[] constructors = clazz.getConstructors(); //获取所有构造函数(public,private,protected) Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
- 反射创建对象,先通过getConstructor()根据参数表类型选择对应构造函数,执行constructor.newInstance()传入参数参数表value,并创建对象。
@SuppressWarnings("unchecked") Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); TestClass testClass = (TestClass) constructor.newInstance("反射调用构造函数", 1);//创建对象 public class TestClass { private TestClass(String message, int mode) { Log.d("tag", message); } ... }
- 反射获取Field,getFields()获取public成员,getDeclaredFields()获取全部成员。通过clazz.getField(fieldName)可以获取指定域名的成员(public),非public成员只能通过clazz.getDecalredField(fieldName)获取,否则会抛NoSuchFieldException。如果要操作某个成员,需要先获得对象实例intance【可通过getConstructor().getInstance()得到】,设置field值:field.set(instance, value)给field设置实参value;获取filed值:field.get(instance)将结果强转成对应类型。注意:如果操作的field是private修饰的,需要执行field.setAccessible(true),解除私有限制,否则会抛IlegalAccessException,更进一步地,就算不是操作的不是private成员,也有必要设置accessible=true,这样可以绕过私有检查,操作速度将提升20倍。
private void testFields(Class clazz){ //获取public字段 Field[] fields = clazz.getFields(); //获取所有字段 Field[] declaredFields = clazz.getDeclaredFields(); for (Field field : fields) { Log.d("tag", field.getName()); } for (Field field : declaredFields) { Log.d("tag", field.getName()); } }
try { TestClass instance = (TestClass) clazz.getConstructor().newInstance(); //反射设置filed值 Field numberField = clazz.getField("number");//number是public field numberField.set(instance, 1); //反射获取filed值 Field nameField = clazz.getDecarledField("name");//name是private field nameField.setAccessible(true);//解除私有限制 String nameValue = (String) nameField.get(instance); } catch (Exception e) { e.printStackTrace(); }
-
反射调用method,与操作field类似,getMethods()获取public方法(包含父类public方法),getDeclaredMethods()获取全部方法(自己申明的,不包括继承的)。通过getMethod("methodName")获取指定方法名的方法(public),通过getDeclaredMethod("methodName")获取指定的非public方法。再创建类实例instance后,就可以通过method.invoke(instance)调用方法。如果方法是含参的,则在反射获取方法时,需要指明方法参数,method.getDeclaredMethod("method", Object.class),并在调用时指明实参method.invoke(instance, 'params')。
@SuppressWarnings("unchecked") private void testMethod(Class clazz) { //获取public方法 Method[] methods = clazz.getMethods(); //获取全部方法 Method[] declaredMethods = clazz.getDeclaredMethods(); try { //创建实例 TestClass instance = (TestClass) clazz.getConstructor().newInstance(); //获取public方法 Method mTestReflect = clazz.getMethod("testReflect"); //获取非public方法 Method mInit = clazz.getDeclaredMethod("init"); //获取一个名为test,参数为string类型的方法 Method mTest = clazz.getDeclaredMethod("test", String.class); //调用方法 mTestReflect.invoke(instance); mInit.invoke(instance); mTest.invoke(instance, "test invoke method by reflect");//调用方法,并传入方法实参 } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } }
- 反射调用类的静态方法、静态成员,只需要把传入的instance改为null。
- 反射能不能给类添加方法?答案:不能。反射执行时,类都编译过了,加载到虚拟机了,如何还能添加方法,就如同番茄鸡蛋都炒熟开吃了,如何还能再加番茄鸡蛋。
- 反射能不能修改final常量?答案:当然能,final是编译时的标记,反射是在运行时修改,反射能实现范型擦除也是这个原因。然而要注意,修改后的私有常量,调用方拿到的结果可能还是没有修改的。JVM在把.java编译成.class时,会对部分类型的常量优化成具体的值,包括int、long、boolean、String类型,其中诸如Integer、Long、Boolean则不会优化。看了下面的代码,你就会明白一切。参考:https://juejin.im/post/598ea9116fb9a03c335a99a4
1.定义常量
public class TestClass { //String 会被 JVM 优化 private final String FINAL_VALUE = "FINAL"; public String getFinalValue() { //剧透,会被优化为: return "FINAL" ,拭目以待吧 return FINAL_VALUE; } }2.反射修改常量
/** * 修改对象私有常量的值 * 为简洁代码,在方法上抛出总的异常,实际开发别这样 */ private static void modifyFinalFiled() throws Exception { //1. 获取 Class 类实例 TestClass testClass = new TestClass(); Class mClass = testClass.getClass(); //2. 获取私有常量 Field finalField = mClass.getDeclaredField("FINAL_VALUE"); //3. 修改常量的值 if (finalField != null) { //获取私有常量的访问权 finalField.setAccessible(true); //调用 finalField 的 getter 方法 //输出 FINAL_VALUE 修改前的值 System.out.println("Before Modify:FINAL_VALUE = " + finalField.get(testClass)); //修改私有常量 finalField.set(testClass, "Modified"); //调用 finalField 的 getter 方法 //输出 FINAL_VALUE 修改后的值 System.out.println("After Modify:FINAL_VALUE = " + finalField.get(testClass)); //使用对象调用类的 getter 方法 //获取值并输出 System.out.println("Actually :FINAL_VALUE = " + testClass.getFinalValue()); } }3.结果 Before Modify:FINAL_VALUE = FINAL After Modify:FINAL_VALUE = Modified Actually :FINAL_VALUE = FINAL
结果出来了:
第一句打印修改前
FINAL_VALUE
的值,没有异议;第二句打印修改后常量的值,说明
FINAL_VALUE
确实通过反射修改了;第三句打印通过
getFinalValue()
方法获取的FINAL_VALUE
的值,但还是初始值,导致修改无效!不信的话,看看字节码吧。 - 如何在AndroidStudio快速查看.java文件对应的.class呢?当project编译过(build success)后,app/build/javac/debug/目录下,目录中放着所有源文件编译生成的.class文件。
10.多线程中的空指针问题
- 导致问题的方法如下,在判空后,却得到model对象抛NullPointerException。
private void work() { Log.d("tag", getThreadId() + "start work()"); if (model != null) { Log.d("tag", getThreadId() + "id=" + model.getId()); Log.d("tag", getThreadId() + "name=" + model.getName());//model throw NullPointerException Log.d("tag", getThreadId() + "flag=" + model.getFlag()); } }
- 首先定位问题为:多线程操作model对象,使得其中当前线程判model不为空后,另一个线程又置model=null,这时当前线程操作work方法还未结束,仍然读操作了model对象。便出现在判空后,仍然得到空指针异常。
private Runnable workRunnable = new Runnable() { @Override public void run() { try { work(); Thread.sleep(1000); } catch (Exception e) { Log.e("tag", e.getMessage()); } } }; private Runnable resetRunnable = new Runnable() { @Override public void run() { Log.d("tag", getThreadId() + "model=null"); model = null; } };
- 【误解】有人说用关键字synchornized声明work()为同步方法。这样只是保持所有操作work()方法的线程同步,却不能保证对model变量的修改能够在线程间同步。
private synchronized void work() { ... }
- 【误解】有人说对model变量用volatile修饰。volatile修饰只能保证model的修改,能够立即被所有线程可见,这时work()方法中,在判model不为空后再操作model,model还是可能被其他线程修改。
private volatile TestModel model;
- 【解决】读取操作都加上锁(同一个锁对象),保证读写一致性。
private synchronized void work() { Log.d("tag", getThreadId() + "start work()"); if (model != null) { Log.d("tag1", getThreadId() + "id=" + model.getId()); Log.d("tag2", getThreadId() + "name=" + model.getName()); Log.d("tag3", getThreadId() + "flag=" + model.getFlag()); } } private Runnable resetRunnable = new Runnable() { @Override public void run() { synchronized (this) { Log.d("tag_reset", getThreadId() + "model=null"); model = null; } } };
- 【解决】读取操作都加上锁(同一个锁对象),保证读写一致性。对变量使用volatile修饰,然后读操作可以在判空后再加锁,减少锁范围/开销。
private volatile TestModel model; private void work() { Log.d("tag", getThreadId() + "start work()"); if (model != null) { synchronized (this) { Log.d("tag1", getThreadId() + "id=" + model.getId()); Log.d("tag2", getThreadId() + "name=" + model.getName()); Log.d("tag3", getThreadId() + "flag=" + model.getFlag()); } } } private Runnable resetRunnable = new Runnable() { @Override public void run() { synchronized (this) { Log.d("tag_reset", getThreadId() + "model=null"); model = null; } } };
- 【解决】不用锁也能保证同步,一个局部变量就搞定了,注意变量要被volatile修饰。
private volatile TestModel model; private void work() { Log.d("tag", getThreadId() + "start work()"); TestModel model = this.model;//使用局部变量,保证model变量的线程同步 if (model != null) { Log.d("tag1", getThreadId() + "id=" + model.getId()); Log.d("tag2", getThreadId() + "name=" + model.getName()); Log.d("tga3", getThreadId() + "flag=" + model.getFlag()); } }
下一篇:android日记(三)