android日记(九)
上一篇:android日记(八)
1.在android代码中执行命令
- java.lang.Runtime类可以允许代码在执行时,执行一段命令行命令。
Runtime.getRuntime().exec(command)
- 封装工具方法,执行命令,读执行流结果。
private fun shellExec(command: String) { val sb = StringBuilder() var mReader: BufferedReader? = null try { Log.d("ANDROID_COMMAND", "exec $command") val mProcess = Runtime.getRuntime().exec(command) mReader = BufferedReader(InputStreamReader(mProcess.inputStream)) var line: String? while ((mReader.readLine()).also { line = it } != null) { sb.append(line + "\n") } Log.d("ANDROID_COMMAND", "exec $command success, result = $sb") } catch (e: IOException) { Log.d("ANDROID_COMMAND", "exec $command failed, exception = ${e.message}") e.printStackTrace() } finally { mReader?.close() sb.clear() } }
- 很多命令是需要设备root的,比如Runtime.exec("su"),执行完会报“Permission Denied 13”。
- 少数命令是不需要root的,比如查500行日志,shellExec("logcat -t 500")。
2.本地打包aar库
- 有时候需要把项目中的某个组件抽离出去,供外部使用,这时需要可以选择aar库的形式。
- 首先将这个要打包组件整理到一个单独的module中。
- 然后在对应的modulede Tasks执行build assemble。
- 执行完后在该module的build/outputs/aar中就可以看到打包生成的aar库文件了,直接导出即可。
- 在打包本地aar库时,如果aar本身有依赖外部第三库的话,打包生成的aar是不会将自己依赖的外部库也打进aar的。这时,如果要使用该aar时,就会报外部库不存在的错误。解决办法是,在引入aar的项目中加上aar所需要的依赖,或者在制作aar时不要引入外部三方库,而是将外部库的源码集成进来。
3.配置本地库依赖
- 通常本地依赖库有aar、jar、so等文件形式。
- 在每个module下都会有个libs目录,本地库就放在这里。
- 引用单个本地依赖库。
implementation files('libs/scansdk-release.aar')
- 有时需要引入的依赖库比较多,也可以以fileTree的形式进行引入。其中,通过dir指定依赖库的目录,通过include指定需要引入哪些库,通过exclude指定要排除的库,指定时可以使用通配符进行匹配。
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'], exclude: 'android-support-v4.jar')
- so库往往需要根据不同的架构模式做不同的库配置,分别在不同的架构的目录中放置对应的so库即可。
- so库只有在被加载后才能使用,通过下面的方法,传入so库的路径进行加载。
System.loadLibrary(soLibName);
- 有时候,本地加载的库文件可能在不同场景下是不同的,可以通过配置库文件目录的sourceSet实现。比如debug和release下要使用不同的库。
sourceSets { main { if (Boolean.parseBoolean(IS_DEBUG)) { jniLibs.srcDirs = ['libs', 'debuglibs'] } else { jniLibs.srcDirs = ['libs', 'releaselibs'] } } }
4.将本地库发布到github maven
- 这里主要是针对没有私人maven仓库的情况,所有采用github的maven库。
- 新建一个android project,并新建一个module,作为需要发布的组件。
- project的build.gradle添加github的maven插件。
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
- 需要发布的module的build.gradle添加github maven插件。
plugins { id 'com.android.library' ... id 'com.github.dcendents.android-maven' }
- 需要发布的module的build.gradle申明group配置。
group = 'com.github.xxx'
- 在github上新建一个仓库,并把本地工程推到github上。这时,可以看到github上的Release入口,进入后创建一个新的发布版本。
点击publish release即完成发布。
-
到jitpack上进行打包,用github账号登录。进去后选择要打包的项目,可以看到该项目在github上创建的各个release版本。jitpack会自动进行打包,如果打包成功的话,就是绿色状态,失败则为红色。
- 如果打包失败,查看日志发现报错如下:
FAILURE: Build failed with an exception. * Where: Build file '/home/jitpack/build/bqtutils/build.gradle' line: 1 * What went wrong: A problem occurred evaluating project ':bqtutils'. > Failed to apply plugin [id 'com.android.library'] > Minimum supported Gradle version is 4.10.1. Current version is 4.8.1. If using the gradle wrapper, try editing the distributionUrl in /home/jitpack/build/gradle/wrapper/gradle-wrapper.properties to gradle-4.10.1-all.zip
这是因为jitpack的gradle版本很久没更新,太老了,如果项目中使用的gradle版本比jitpack的高,那就会遇到这个问题。
- 一种解决办法要么把项目的gradle版本降级回去,但这样体验非常不好,并且有些新项目已经不允许那么老的gradle版本了。
- 其实还有一种办法就是让jitpack使用项目中的gradle工具进行打包,那么project上传github时一定要注意,不能漏掉下面的gradle相关文件。
5.为应用签名
- 为什么要签名?不签名的app不能发布。如何签名?官方文档有说明。
- 比如编译过程中,在build/output/目录下生成的apk文件,如果直接导出进行安装,会提示“安装失败,不是正式包”。问题其实是apk没有进行签名的缘故。
- 那如何打签名包了?AndroidStudio Build目录下Generated Signed APK,进入后会让选择签名文件,选择好签名文件就可以打包了。
- 可是现在项目中还没有签名文件?那也不要紧,可以点击Create New,进入下面的面板,创建一个新的签名文件。
-
然而问题来了,创建签名报错了。
这是因为输入的签名文件没有带签名文件的后缀,后缀通常是“.jks”、“.keystore”,补充后缀后重新创建。
-
创建签名文件时,一定记好设定的alias、storePassword、keyPassword,因为后面打包时需要填入这些信息进行校验。如果担心记不住,可以在项目中新建signed.properties,记录这些信息。
keyAlias=myKey keyPassword=123456 storePassword=123456
- 直接打开签名文件只会看到乱码,需要通过keytool工具查看签名文件的具体信息,包括alias,有效期等。
keytool -list -v -keystore key.jks
执行命令时需要输入storePassword,输入时命令行不会回显输入的字符,尽管输就好了,输完回车即可。
- 在打签名包前,还需要到build.gradle中配置签名。通常为了安全起见,debug包和release包需要使用不同的签名,那么就再去创建一个release的签名文件,然后更新build.gradle中的配置。
defaultConfig { ... signingConfigs { debug { storeFile rootProject.file("key.jks") storePassword "123456" keyAlias "myKey" keyPassword "123456" } release { storeFile rootProject.file("key.jks") storePassword "123456" keyAlias "myKey" keyPassword "123456" } }
- 除了通过AndroidStudio的可视化面板创建签名文件,也可以通过keytool命令进行。
keytool -genkey -alias myKey -keypass 123456 -keyalg RSA -keysize 2048 -validity 36500 -keystore key.jks -storepass 123456
执行命令时,也可以不一次性输入完整的信息,当信息不完整时,终端会在命令行中进行交互完成剩余的信息。比如输入的命令没有指定storePassword,执行命令时就会提示让输入storePassword。
xxxMacBook-Pro sso_login_android % keytool -genkey -alias debug_key -keypass sso@demo -keyalg RSA -keysize 2048 -validity 36500 -keystore key.jks 输入密钥库口令: 再次输入新口令: 警告: PKCS12 密钥库不支持其他存储和密钥口令。正在忽略用户指定的-keypass值。 您的名字与姓氏是什么? [Unknown]: 您的组织单位名称是什么? [Unknown]: ctrip 您的组织名称是什么? [Unknown]: 您所在的城市或区域名称是什么? [Unknown]: 您所在的省/市/自治区名称是什么? [Unknown]: 该单位的双字母国家/地区代码是什么? [Unknown]: CN=Unknown, OU=ctrip, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正确? [否]: y
填完信息后,会让你确认信息,这时需要输入y进行确认。
6.GenSignature是如何获取签名的?
- 获取签名
private String gen(String packageName) { String sign = ""; try { Signature[] signatures = getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures; if (signatures.length > 0) { MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(signatures[0].toByteArray()); BigInteger bigInteger = new BigInteger(1, digest.digest()); sign = bigInteger.toString(16); Log.d("msg", "获取到包名 "+packageName+" 下的签名为: " + sign); } } catch (Exception e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "获取失败!", Toast.LENGTH_SHORT).show(); } return sign; }
7.LayoutInflater解析xml的过程。
- 当activity在onCreate()中,通过setContentView()传入layout时,会执行到基类AppCompatDelegateImpl#setContentView()。
@Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.getWrapped().onContentChanged(); }
- 这时LayoutInflater就开始登场了,首先是通过LayoutInflater.from()构建实例。
public static LayoutInflater from(Context context) { LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (LayoutInflater == null) { throw new AssertionError("LayoutInflater not found."); } return LayoutInflater; }
而不是通过下面的构造函数创建实例,
protected LayoutInflater(LayoutInflater original, Context newContext) { mContext = newContext; mFactory = original.mFactory; mFactory2 = original.mFactory2; mPrivateFactory = original.mPrivateFactory; setFilter(original.mFilter); initPrecompiledViews(); }
并且创建过程中没有再额外设置过mFactory,mFactory2和mPrivateFactory。
public void setFactory2(Factory2 factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = mFactory2 = factory; } else { mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2); } }
这样可以得出的结论是,在创建LayouterInflater实例的过程中,没有设置过Factory和Factory2,也就是LayouterInflater中的mFactory和mFactory2至始至终是null。
- 然后进行inflater.inflate()方法的解析工作,方法首先会判断一些特殊节点,比如<merge/>标签。对于正常的view节点,会进行两个关键操作,一是通过createViewFromTag()方法创建view,二是通过rInflateChildren()方法创建ViewGroup的子View。
/** * Inflate a new view hierarchy from the specified XML node */ public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { ... View result = root; try { advanceToRootNode(parser); final String name = parser.getName();if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, inflaterContext, attrs, false); } else { // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) {// Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } ...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. if (root == null || !attachToRoot) { result = temp; } } } catch (Exception e) { ... } return result; } } - 先看createViewFromTag()创建view的具体过程,
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) { ...try { View view = tryCreateView(parent, name, context, attrs); if (view == null) { ...if (-1 == name.indexOf('.')) { view = onCreateView(context, parent, name, attrs); } else { view = createView(context, name, null, attrs); } ... } return view; } catch (Exception e) { ... } }
这个方法里会先执行tryCreateView方法,这里就会依赖mFactory,mFactory2及mPrivateFactory,经过前面的分析,这三个变量都为null,所以这个方法最终其实会返回的view是null。
public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (name.equals(TAG_1995)) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); } View view; if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } return view; }
- 因此creatViewFromTag()方法里也没有真正创建view,而是进入到onCreateView()或者createView()中进行创建,这两个方法最终都会执行到下面的createView()方法中。
public final View createView(@NonNull Context viewContext, @NonNull String name,@Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException { Objects.requireNonNull(viewContext); Objects.requireNonNull(name); Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); ... constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } ...try { final View view = constructor.newInstance(args); if (view instanceof ViewStub) { // Use the same context when inflating ViewStub later. final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } return view; } finally { mConstructorArgs[0] = lastContext; } } catch (Exception e) { ... } }
这个方法就是真正创建view的实现了,就是根据传入的view name进行反射,创建出view对象。创建过程采用了缓存机制实现,缓存对象是要创建view的构造器,只有当view没有创建过时,时才执行反射创建,并且创建完会将view的构造器存到缓存表中,下一次再需要创建相同的view对象时,就直接从缓存表中取出对应的构造器,创建对象,避免重复反射。
- 到此为此,inflater.inflate()方法中,关键的一步:根据节点标签创建View对象,就完成了。还有另一个关键的步骤,通过rInflateChildren()方法创建ViewGroup的子View完成的,其内部就是执行下面的方法。
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; boolean pendingRequestFocus = false; while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } final String name = parser.getName(); if (TAG_REQUEST_FOCUS.equals(name)) { pendingRequestFocus = true; consumeChildElements(parser); } else if (TAG_TAG.equals(name)) { parseViewTag(parser, parent, attrs); } else if (TAG_INCLUDE.equals(name)) { if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); } else { final View view = createViewFromTag(parent, name, context, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflateChildren(parser, view, attrs, true);//递归调用 viewGroup.addView(view, params); } } if (pendingRequestFocus) { parent.restoreDefaultFocus(); } if (finishInflate) { parent.onFinishInflate(); } }
关键逻辑已标红,会遍历创建出来的View,如果存在子View,那么就会通过递归调用,一层一层地将所有子View通过createViewFromTag()创建出来。创建的子View会被添加到对应的ViewGroup中。就是这样把一个完整布局,View树给创建了出来。
8.LayoutInflater解析布局时,attachToRoot参数应该传true还是false?
- 在inflate()的调用传参,第三个参数怎么确定是false还是true。
inflater.inflate(R.layout.xx_layout, container, false)
- 在inflate()源码中,将节点创建为具体View后,会根据传入的attchToRoot参数为true或false,做不同的处理。
// We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. if (root == null || !attachToRoot) { result = temp; }
当attachToRoot=false时,会返回创建好的布局根View。而当attachToRoot=true时,会将创建的布局View添加到container中,并返回null。
- 也就是说,下面两个操作是等价的。
inflater.inflate(R.layout.fragment_easyride_layout, container, true)
val view = inflater.inflate(R.layout.fragment_easyride_layout, container, false) container.addView(view)
9.为什么自定义View在xml中使用时,需要写完整的包名,也系统View就不需要。
- 现有一个layout.xml中,又一个系统的TextView,还有一个自定义的H5WebView。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="44dp" android:text="@string/app_name" /> <com.example.ssologinandroid.H5WebView android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
public class H5WebView extends WebView { private Context mContext; public H5WebView(Context context) { super(context); init(context); } public H5WebView(Context context, AttributeSet attributes) { super(context, attributes); init(context); } public H5WebView(Context context, AttributeSet attributes, int defStyle) { super(context, attributes, defStyle); init(context); } private void init(Context context) { mContext = context; ... } ... }
- 如果H5WebView在xml不写完整包名会有问题吗?有问题!加载布局时抛了InfalteException,原因是找不到H5WebView。
<H5WebView android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" />
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.ssologinandroid/com.example.ssologinandroid.WebViewActivity}:
android.view.InflateException: Binary XML file line #9 in com.example.ssologinandroid:layout/activity_web_view:
Binary XML file line #9 in com.example.ssologinandroid:layout/activity_web_view: Error inflating class H5WebView - 一切还要从LayoutInflater解析xml开时说起,当执行createViewFromTag()创建View时,会做下面标红代码的判断。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ... try { View view = tryCreateView(parent, name, context, attrs); if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = onCreateView(context, parent, name, attrs); } else { view = createView(context, name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (Exception e) { ... } }
也就是看view标签name中是否含有“.”字符,如果有就执行createView(),没有则执行onCreateView()方法。
- 先看name中不含“.”的情况,onCreateView()的执行时,调用了createView(name, attrs),该方法里又调用了createView(context, name, prefix, attrs),传入prefix=“android.view”。
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs); }
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Context context = (Context) mConstructorArgs[0]; if (context == null) { context = mContext; } return createView(context, name, prefix, attrs); }
- 再看name中含“.”的情况,直接调用了createView(context, name, prefix, attrs)方法,传入的prefix=null。
- 因此不管name中是否含有“.”,都最终会执行createView(context, name, prefix, attrs),只是传入的prefix不同而已。方法内部与prefix产生关联的代码是这句,
clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class);
这句话是根据view标签name反射创建View对象,反射创建对象时,需要完整的类名,否则反射会找不到类。
- 由于系统View都是在android.view包下的,createView()时如果name中不含“.”而添加“android.view”的prefix,得到view的完整名称,因此在xml布局放置系统View标签时,不写完整包名也能正常解析。而对于自定义View,如果xml不写完整的包名,自然也是会被加上“android.view”的包名,而自定为View根本不在android.view包下,反射时自然就找不到类了。
10.自定义View:关于Caused by: java.lang.NoSuchMethodException异常
- 在xml布局中使用自定义View时,解析布局会出现NoSuchMethodException异常,由于解析过程实际是根据标签名反射创建View的过程,可以推断是反射过程抛了异常
- 在LayouterInflater#onCreateView()查看执行反射创建的源码,
public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs){ ... clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);sConstructorMap.put(name, constructor);
...
final View view = constructor.newInstance(args);
return view;}
其中,mConstructorSignature为,
static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class};
也就是反射调用类的参数表为(Context, AttributeSet)的构造函数,创建出对象。
- 显然易见,导致反射报错的原因,就在于自定义View中没有添加参数表为(Context, AttributeSet)的构造函数。
- 总结:自定义View时,如果要在xml布局中使用,一定要加上参数表为(Context, AttributeSet)的构造函数。
下一篇:android日记(十)