apk更新的技术:
- 增量更新:
- 热更新
- 插件化
- rn的js脚本修复apk
- 静默安装
增量更新的流程
- APP检测最新版本,把版本号告诉服务器,服务器进行版本校验
- 如果有新版本,服务器需要对当前版本和新版本apk进行一次差分,产生patch差分文件,或者在新版本上传到服务器的时候已经差分好
- app在后台下载好差分文件,进行md5校验,在本地进行合并(跟/data目录下的apk文件进行合并),合并成新的apk后,提示用户安装。
新司机上路,坑多,本文重点是踩坑,不详细讲retrofit用法,本文不推荐使用信任所有证书的做法。
分为多种格式, bks cer jks等,这里使用的是bks格式证书。
点击网站网址栏前的小锁按钮,选择详细信息,选择view certificate。
显示证书之后,点击详细信息,然后一直下一步,直到导出.cer文件。






做法:1,下载特定版本的JCE Provider包
http://pan.baidu.com/s/1c1ur13y
or
http://www.bouncycastle.org/download/bcprov-jdk15on-146.jar (现在连接失效)
2,命令行输入以下命令
keytool -importcert -v -trustcacerts -alias 位置1 \
-file 位置2 \
-keystore 位置3 -storetype BKS \
-providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath 位置4 -storepass 位置5
位置1:是个随便取的别名
位置2:cer或crt证书的全地址
位置3:生成后bks文件的位置,建议写全地址
位置4:上面下载JCE Provider包的位置
位置5:生成后证书的密码。下边获取sslsocketfactory中会用到密码
以下例子:
keytool -importcert -v -trustcacerts -alias xx -file E:\bks\xx.cer -keystore E:\bks\xx.bks -storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath E:\bks\bcprov-jdk15on-146.jar -storepass xxxxxx
成功之后会在你指定的位置生成bks文件.然后将文件放到项目raw目录下。
这里是https证书认证最关键的代码,一定要仔细查看。password和设置keystore的bks类型一定不要搞错。
/**
* 获取bks文件的sslsocketfactory
* @param context
* @return
*/
public static SSLSocketFactory getSSLSocketFactory(Context context) {
final String CLIENT_TRUST_PASSWORD = "123456";//信任证书密码,该证书默认密码是123456
final String CLIENT_AGREEMENT = "TLS";//使用协议
final String CLIENT_TRUST_KEYSTORE = "BKS";
SSLContext sslContext = null;
try {
//取得SSL的SSLContext实例
sslContext = SSLContext.getInstance(CLIENT_AGREEMENT);
//取得TrustManagerFactory的X509密钥管理器实例
TrustManagerFactory trustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
//取得BKS密库实例
KeyStore tks = KeyStore.getInstance(CLIENT_TRUST_KEYSTORE);
InputStream is = context.getResources().openRawResource(R.raw.traint);
try {
tks.load(is, CLIENT_TRUST_PASSWORD.toCharArray());
} finally {
is.close();
}
//初始化密钥管理器
trustManager.init(tks);
//初始化SSLContext
sslContext.init(null, trustManager.getTrustManagers(), null);
} catch (Exception e) {
e.printStackTrace();
Log.e("SslContextFactory", e.getMessage());
}
return sslContext.getSocketFactory();
}
String baseUrl = "https://skyish-test.yunext.com";
int[] certificates = {R.raw.traint};
String[] hostUrls = {baseUrl};
OkHttpClient client = new okhttp3.OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.sslSocketFactory(HTTPSUtils.getSSLSocketFactory(context))
//.hostnameVerifier(HTTPSUtils.getHostNameVerifier(hostUrls))
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.build();
Retrofit retrofit = new Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(client)
.build();
配置好retrofit之后就可以使用了。
03-08 15:17:26.804 21672-21672/com.qiwo.enumlistdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.qiwo.enumlistdemo, PID: 21672
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.qiwo.enumlistdemo/com.qiwo.enumlistdemo.RetrofitHttpsDemoActivity}: java.lang.IllegalStateException: SSLContext is not initialized.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2650)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2720)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
at android.os.Handler.dispatchMessage(Handler.java:111)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5917)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)
Caused by: java.lang.IllegalStateException: SSLContext is not initialized.
at com.android.org.conscrypt.OpenSSLContextImpl.engineGetSocketFactory(OpenSSLContextImpl.java:107)
at javax.net.ssl.SSLContext.getSocketFactory(SSLContext.java:358)
at com.qiwo.api.HTTPSUtils.getSSLSocketFactory(HTTPSUtils.java:158)
at com.qiwo.api.DemoHttpsApi.<init>(DemoHttpsApi.java:40)
at com.qiwo.enumlistdemo.RetrofitHttpsDemoActivity.initViewAndListener(RetrofitHttpsDemoActivity.java:37)
at com.doudou.common.base.BaseSwipeBackAppcompatActivity.onCreate(BaseSwipeBackAppcompatActivity.java:68)
at android.app.Activity.performCreate(Activity.java:6307)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1113)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2603)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2720)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
at android.os.Handler.dispatchMessage(Handler.java:111)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5917)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)
CLIENT_TRUST_PASSWORD是证书的密码,必须与生成证书步骤里的设置的证书密码一致。如下:
public static SSLSocketFactory getSSLSocketFactory(Context context) {
final String CLIENT_TRUST_PASSWORD = "123456";//信任证书密码,该证书默认密码是changeit
final String CLIENT_AGREEMENT = "TLS";//使用协议
final String CLIENT_TRUST_KEYSTORE = "BKS";
SSLContext sslContext = null;
// ...
}
如果是cer类型证书,需要使用生成bks方法重新生成bsk类型证书。
服务器主机名认证失败
((HttpsURLConnection) urlConnection).setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
//.hostnameVerifier(HTTPSUtils.getHostNameVerifier(hostUrls)) 注释掉这句代码
SSL链接时主机名验证失败
//.hostnameVerifier(HTTPSUtils.getHostNameVerifier(hostUrls)) 注释掉这句代码
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for cert
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:333)
at okhttp3.internal.io.RealConnection.connectTls(RealConnection.java:239)
at okhttp3.internal.io.RealConnection.establishProtocol(RealConnection.java:196)
at okhttp3.internal.io.RealConnection.buildConnection(RealConnection.java:171)
at okhttp3.internal.io.RealConnection.connect(RealConnection.java:111)
at okhttp3.internal.http.StreamAllocation.findConnection(StreamAllocation.java:187)
at okhttp3.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:123)
at okhttp3.internal.http.StreamAllocation.newStream(StreamAllocation.java:93)
at okhttp3.internal.http.HttpEngine.connect(HttpEngine.java:296)
at okhttp3.internal.http.HttpEngine.sendRequest(HttpEngine.java:248)
at okhttp3.RealCall.getResponse(RealCall.java:243)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:201)
at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:212)
at okhttp3.RealCall$ApplicationInterceptorChain.proceed(RealCall.java:190)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:163)
at okhttp3.RealCall.execute(RealCall.java:57)
at retrofit2.OkHttpCall.execute(OkHttpCall.java:174)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.
at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80)
at rx.Subscriber.setProducer(Subscriber.java:211)
at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76)
at rx.Subscriber.setProducer(Subscriber.java:205)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.ja
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.ja
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
at rx.Observable.unsafeSubscribe(Observable.java:8666)
at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94)
at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecut
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust
at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:324)
at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:225)
at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:115)
at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:571)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:329)
... 35 more
Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
使用了错误的证书。证书验证失败。
重新生成证书
之前我是看的Tamic的做法,不能走通,不推荐使用它的那种做法。如果是使用它的那种做法,出现错误,请按照本文的做法,使用HTTPS。
webview可以加载本地和网络页面,根据html的文件位置,有不同的写法.
mWebView.loadUrl("www.baidu.com");
mWebView.loadUrl("file:///android_res/test.html");
通常情况下,webview会重新设置webchromeclient,以便在本应用内实现页面跳转.
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
mWebView.loadUrl(url);
return true;
}
});
另外,要实现本文的与h5交互必须允许使用js接口,在实际开发中,一般都要加上这一句.
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
调用js方法有两种情况: 如果调用js的无返回值方法, 可以直接使用load方法
mWebView.loadUrl("javascript:do()");
如果要调用有返回值方法,需要调用evaluatjavascript方法
mWebView.evaluateJavascript("sum(1,2)", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Toast.makeText(ViewPagerGalleryDemoActivity.this, "value = " + value, Toast.LENGTH_SHORT).show();
}
});
js代码如下:
<script type="text/javascript">
function sum(a,b){
return a+b;
}
function do(){
document.getElementById("p").innerHTML="hello world";
}
</script>
需要在Android中定义javascriptinterface接口和声明
首先要添加一个类和方法,并且用javascriptinterface
public class TestJavaScriptInterface {
@JavascriptInterface
private String testJavaScriptInterface () {
return "hello javascript"
}
}
打开javascriptinterface给h5的开关
mWebView.addJavascriptInterface(new TestJavaScriptInterface(), "android");
js代码如下:
<script type="text/javascript">
function s(){
//调用Java的back()方法
var result =window.android.testJavaScriptInterface();
document.getElementById("p").innerHTML=result;
}
</script>
什么是adb,c/s架构,adb命令分为3种
adb命令是程序自带的一些命令,adb shell是调用Android系统中的命令,这些系统是放在system/bin目录下
// 根据TAG和级别过滤日志输出
adb logcat [-s] [ClassName:[PREVISOUS]] [*:[PREVIOUS]]
adb logcat // 直接输出的终端
adb logcat > c:/log.txt // 保存到文件
adb logcat ActivityManager:I PowerManager:D *:S
adb logcat *:W // 显示所有优先级大于等于“warning”的日志
adb logcat -s ActivityManager
logcat命令列表:
-d 将日志显示到控制台后退出
-c 清理已经存在的目录
-f <filename> 将日志输出到文件
-v <format> 设置日志输出格式控制输出字段,格式如下,默认是brief格式
brief--显示优先级/标记和原始进程PID
process--仅显示进程PID
tag--仅显示优先级/标记
thread--仅显示进程:线程和优先级/标记
raw--显示原始的日志信息,调用时间,优先级/标记,PID
time--显示日期,调用时间,优先级标记/pid
long--显示所有的元数据并用空行分割消息内容
-b <buffer> 加载一个可使用的日志缓冲区供查看,默认是main
radio--查看包含在无线/电话相关的缓冲区信息
events--查看事件相关的消息
main--查看主缓冲区
adb logcat -f c:/log.txt
adb logcat -v thread // 使用thread输出格式
adblogcat -b radio
/system/bin下或者sdk sources/android-20/com/android/commands
adb shell pm list package -i
adb shell pm path com.tencent.mobileqq
adb shell pm dump com.tencent.mobileqq
adb shell am start -n com.android.camera/.Camera
修改系统设置
探究下 Android4.2 中新增的 settings 命令
| 命令 | 功能 |
|---|---|
| package | 包查询 |
| activity | 所有activity信息 |
| connectivity | 网络连接 |
| netpolicy | 网络策略 |
| netstats | 网络状态 |
| wifi | wifi信息 |
| network_manager | 网络管理 |
| account | 账号信息 |
| alarm | 闹钟信息 |
| meminfo | 内存信息 |
| cpuinfo | cpu使用情况 |
| gfxinfo | 帧率信息 |
| display | 显示 |
| power | 电源 |
| batterystats | 电池状态 |
| battery | 电池 |
| batteryinfo $package_name | 电量信息及CPU使用时长 |
| diskstats | 磁盘相关信息 |
| usagestats | 每个界面的启动时间 |
| statusbar | 状态栏 |
| alarm | 闹钟 |
| location | 位置 |
| window | 窗口 |
adb shell log [-p PREVIOUS] [-t TAG] [MESSAGE]
查看Android设备的参数信息
adb shell getprop [key]
// 常用命令
cat,cd,chmod,cp,data,df,du,grep,kill,ln,ls,lsof,netstat,ping,ps,rm,rmdir,top,touch,>,>>,|
e-pub的意思是电子出版,是IDPF制定的一个标准,这个标准是基于xml格式的电子书或者其它数字出版物。
.epub文件其实就是一个简单的zip文件可以使用winrar或者linux使用归档管理器打开来看目录结构和基本文档。内容都是xml格式的。
这个文件十分简单,里面只有
application/epub+zip
但是这个文件必须作为zip文件中的第一个文件,并且不能被压缩,必须在根目录下。同时这个文件不能有回车和换行。
META-INFO文件夹跟mimetype一样,也是必须存在的文件夹,也必须在根目录下,在文件夹下有一个container.xml文件.epub阅读系统会首先查看该文件,他指向文件图书的源数字文件的位置。这个container.xml格式如下
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf"
media-type="application/oebps-package+xml" />
</rootfiles>
</container>
mimetype 和 container 是 EPUB 档案中仅有的两个需要严格限制位置的文件。建议将其他文件保存到 EPUB 的子目录下(通常被称为OEBPS但不是必须的)。
META-INFO文件夹下的container.xml所指定的rootfile的文件。该文件文件名没有特殊要求,扩展名为.opf。他指定了图书中所有内容的位置,如文本和多媒体目录。另外他还会给出一个元数据文件,它是Navigation Center eXtended (NCX) 表,有的称其为逻辑目录,opf内容示例如下所示:
<?xml version='1.0' encoding='utf-8'?>
<package xmlns="http://www.idpf.org/2007/opf"
xmlns:dc="http://purl.org/dc/elements/1.1/"
unique-identifier="bookid" version="2.0">
<metadata>
<dc:title>Hello World: My First EPUB</dc:title>
<dc:creator>My Name</dc:creator>
<dc:identifier id="bookid">urn:uuid:12345</dc:identifier>
<meta name="cover" content="cover-image" />
</metadata>
<manifest>
<item id="ncx" href="toc.ncx" media-type="text/xml"/>
<item id="cover" href="title.html" media-type="application/xhtml+xml"/>
<item id="content" href="content.html" media-type="application/xhtml+xml"/>
<item id="cover-image" href="images/cover.png" media-type="image/png"/>
<item id="css" href="stylesheet.css" media-type="text/css"/>
</manifest>
<spine toc="ncx">
<itemref idref="cover" linear="no"/>
<itemref idref="content"/>
</spine>
<guide>
<reference href="cover.html" type="cover" title="Cover"/>
</guide>
</package>
文档本身必须使用命名空间 xmlns=”http://www.idpf.org/2007/opf“,
metadata也可以在这里指定命名空间,元数据一般使用 Dublin Core Metadata Initiative (DCMI)
xmlns=”http://purl.org/dc/elements/1.1/“
Dublin Core Metadata Initiative (DCMI)定义了一组常见的元数据,可以用来描述各种不同的数字资料,下边是元数据摘要:
...
<metadata>
<dc:title>Hello World: My First EPUB</dc:title>
<dc:creator>My Name</dc:creator>
<dc:identifier id="bookid">urn:uuid:12345</dc:identifier>
<meta name="cover" content="cover-image" />
</metadata>
...
有两个术语是必须的,分别是title和identifier。这个identifier必须是唯一的,对于图书出版商来说这个字段一般包括ISBN编号,可以使用URL或者很大的随机生成的uuid。
属性unique-identifier的值必须和dc:identifier元素的id属性匹配
epub规范没有要求包含name的属性值为cover的meta元素,但是为了增加封面和图像的可移植性,一般建议加上。meta元素的content属性的值对应在这个opf文件manifest元素的id号,
opf文件中的manifest节点列出了epub的所有资源,通常是组成电子书的一组xhtml文件和相关的媒体文件。epub鼓励使用css设定图书内容的样式,因此manifest中也包含css。进入数字图书的所有内容必须在manifest中列出。
下边是manifest示例:
...
<manifest>
<item id="ncx" href="toc.ncx" media-type="text/xml"/>
<item id="cover" href="title.html" media-type="application/xhtml+xml"/>
<item id="content" href="content.html" media-type="application/xhtml+xml"/>
<item id="cover-image" href="images/cover.png" media-type="image/png"/>
<item id="css" href="stylesheet.css" media-type="text/css"/>
</manifest>
所有的想必须有对应的media-type
epub支持三种核心图像文件类型:JPEG,PNG,GIF和Scalable Vector Graphics(SVG矢量图)
spine的意思是脊柱,作用是线性阅读顺序,可以将OPF spine看做书中的页面的顺序,按照文档顺序从上到下一次读取spine,下边是spine示例:
...
<spine toc="ncx">
<itemref idref="cover" linear="no"/>
<itemref idref="content"/>
</spine>
...
每个itemref元素都需要一个idref属性,并且和manifest中的id想匹配。toc属性也是必须的,他引用manifest中表示ncx表文件名的id。
spine中的linear属性表明该项作为线性阅读顺序的一部分,还是和先后顺序无关。建议将封面定义为no。符合epub规范的阅读系统将首次打开spine中设置linear=no的第一项。
这个是可选的,最好保留,可以为epub阅读系统提供语义功能。下边是示例:
...
<guide>
<reference href="cover.html" type="cover" title="Cover"/>
</guide>
...
上边就是opf文件中的所有内容,manifest文件定义epub所有物理资源,spine定义这些资源的顺序信息,guide负责解释这些部分的含义。
ncx定义了数字图书的目录表,在复杂的图书中,目录表通常采用层次结构,包括嵌套的内容,章节等信息。
下边是简单的toc.ncx文件内容:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN"
"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
<meta name="dtb:uid" content="urn:uuid:12345"/>
<meta name="dtb:depth" content="1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle>
<text>Hello World: My First EPUB</text>
</docTitle>
<navMap>
<navPoint id="navpoint-1" playOrder="1">
<navLabel>
<text>Book cover</text>
</navLabel>
<content src="title.html"/>
</navPoint>
<navPoint id="navpoint-2" playOrder="2">
<navLabel>
<text>Contents</text>
</navLabel>
<content src="content.html"/>
</navPoint>
</navMap>
</ncx>
EPUB使用的DAISY的NCX DTD的命名空间
xmlns=”http://www.daisy.org/z3986/2005/ncx/“
DTD要求ncx文件的head元素包括四个meta元素:
是图书的标题,和opf文件中的dc:title对应
navMap是ncx文件中最重要的部分,定义了电子书的目录。navmap中包括一个或者多个navPoint元素,每个navPoint都要包括下列元素:
由于 NCX 源自其他标准,使用 NCX 编码的信息和 OPF 内容之间存在重复。如果通过程序生成 EPUB,这算不上什么问题,因为同样的代码可输出到两个文件中。两个位置的信息要一致,不同的 EPUB 读者可能使用不同位置的值。(可如果手工去编写呢?)
opf spine主要用来描述电子书章节的顺序,比如第一章后第二章再后第三章 …
ncx文件描述电子书的目录结构。
一条法则就是ncx包括的节点比spine多。实际上spine的所有项都会出现在ncx中,但是ncx可能更详细。
canvas绘制文字有三种方式:1. drawText() 2. drawTextRun() 3. drawTextOnPath()
这个坐标可以认为是在文字左下角。是比较接近的位置,但是如果要精确还是以下边所说的基线为准。
API 23新加入的方法,比drawText有两个新的参数上下文和文字方向, 主要用于文字结构比较特殊的文字的绘制。
沿着一条线来绘制文字
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPaint.setColor(Color.BLUE);
Path path = new Path();
path.moveTo(100, 100);
path.lineTo(200, 200);
path.rLineTo(300, 100);
path.rLineTo(400, 200);
canvas.drawPath(path, mPaint);
canvas.drawTextOnPath("Hello HenCode!", path, 0, 0, mPaint);
方法为drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint), hOffset和vOffset分别是文字相对于path水平方向和垂直方向的偏移量
canvas.drawText()只能绘制单行的文字,而不能换行。
如果要绘制多行文字,必须把文字切断后使用drawText或者一一使用StaticLayout。
他是android.text.Layout的子类,单纯用来绘制文字的。
它可以设置宽度上限来使文字自动换行,或者使用\n使文字换行。
String text1 = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";
StaticLayout staticLayout1 = new StaticLayout(text1, paint, 600,
Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
String text2 = "a\nbc\ndefghi\njklm\nnopqrst\nuvwx\nyz";
StaticLayout staticLayout2 = new StaticLayout(text2, paint, 600,
Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
...
canvas.save();
canvas.translate(50, 100);
staticLayout1.draw(canvas);
canvas.translate(0, 200);
staticLayout2.draw(canvas);
canvas.restore();
setTextSize() // 设置文字大小
setTextScaleX() // 设置文字缩放
setTextAlign() // 设置对齐方式,三种LEFT,RIGHT,CENTER,默认LEFT
setHinting() // 设置字体微调
现在的 Android 设备大多数都是是用的矢量字体。矢量字体的原理是对每个字体给出一个字形的矢量描述,然后使用这一个矢量来对所有的尺寸的字体来生成对应的字形。由于不必为所有字号都设计它们的字体形状,所以在字号较大的时候,矢量字体也能够保持字体的圆润,这是矢量字体的优势。不过当文字的尺寸过小(比如高度小于 16 像素),有些文字会由于失去过多细节而变得不太好看。 hinting 技术就是为了解决这种问题的:通过向字体中加入 hinting 信息,让矢量字体在尺寸过小的时候得到针对性的修正,从而提高显示效果。效果图盗一张维基百科的:

setTextSkewX() // 设置文字倾斜
setTextLocation() // 设置语言区域
setLetterPadding() // 设置字符间距
setFontFeatureSettings() // 设置字体样式
paint.setFontFeatureSettings("scmp"); // 设置为small cap
canvas.drawText("Hello Hencode", 100, 100, paint);
这个东西不懂,以后补充一下
setTypeFace() // 设置字体
paint.setTypeface(Typeface.DEFAULT);
paint.setTypeface(Typeface.SERIF);
paint.setTypeface(Typeface.createFromAssert(getContext().getAssert(), "xx.ttf"))
setUnderlineText() // 是否加下划线
setFakeBoldText() // 设置伪粗体
setStrikeThruText() // 是否加删除线
setElegantTextHeight(boolean elegant) // 是否开启优雅的高度
setSubpixelText() // 是否开启次像素级抗锯齿,就是增强抗锯齿效果。
setLinearText() // 设置liear text
canvas.drawText(texts[0], 100, 150, paint);
canvas.drawText(texts[1], 100, 150 + paint.getFontSpacing, paint);
canvas.drawText(texts[2], 100, 150 + paint.getFontSpacing * 2, paint);


这个不知道什么时候用到
text是文字,start,end是指待测的文本的开始位置和结束位置,rect是测量完要赋值的矩形。
paint.setStyle(Paint.Style.FILL);
canvas.drawText(text, offsetX, offsetY, paint);
paint.getTextBounds(text, 0, text.length(), bounds);
bounds.left += offsetX;
bounds.top += offsetY;
bounds.right += offsetX;
bounds.bottom += offsetY;
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(bounds, paint);

canvas.drawText(text, offsetX, offsetY, paint);
float textWidth = paint.measureText(text);
canvas.drawLine(offsetX, offsetY, offsetX + textWidth, offsetY, paint);

注意: getTextBounds()和measureText()的区别:
如果你用代码分别使用 getTextBounds() 和 measureText() 来测量文字的宽度,你会发现 measureText() 测出来的宽度总是比 getTextBounds() 大一点点。这是因为这两个方法其实测量的是两个不一样的东西。
- getTextBounds()测量的是文字的显示范围,这个矩形恰好能够包裹住文字
- measureText()测量的是文字绘制时所占的宽度,包括了字符间距等
google api是这样写的
Measure the text, stopping early if the measured width exceeds maxWidth.意思是测量文本,如果宽度超过给定的宽度就停止测量。其效果就是如果你的字符串的长度大于breakText()中指定的最大长度,drawtext的时候就只显示这个被截取的定长的文字。返回值为截取的字符的个数。
String text = "Hencode is very very very ... nice!";
mPaint.setTextSize(64);
int i = mPaint.breakText(text.toCharArray(), 0, 17, 600, new float[]{});
canvas.drawText(text.toCharArray(), 0, i, 100, 100, mPaint);

对于像EditText一样的场景,就会需要绘制光标,在API 23开始引入两个新方法来计算光标。(这跟光标没有毛线关系貌似,主要是测量字符的宽度)
| params | des |
|---|---|
| text | CharSequence: the text to measure. Cannot be null. |
| start | int: the index of the start of the range to measure |
| end | int: the index + 1 of the end of the range to measure |
| contextStart | int: the index of the start of the shaping context |
| contextEnd | int: the index + 1 of the end of the shaping context |
| isRtl | boolean: whether the run is in RTL direction |
| offset | int: index of caret position |
return : width measurement between start and offset
即是说 text指的是原文本,start,end指的是要测量的开始和结束的位置,contextStart,contextEnd不知道什么意思, isrtl是否反转字符串,offset指要插入的位置。
返回的是start和offset之间的距离。
mPaint.setTextSize(36);
// 包含特殊符号的绘制(如 emoji 表情)
String text = "Hello HenCoder \uD83C\uDDE8\uD83C\uDDF3"; // "Hello HenCoder 🇨🇳"
int start = 0;
int end = text.length();
int contextStart = 0;
int contextEnd = text.length();
boolean isRtl = false;
int offset = text.length();
float advance = mPaint.getRunAdvance(text, start, end, contextStart, contextEnd, isRtl, offset - 4);
canvas.drawText(text, 100, 100, mPaint);
canvas.drawLine(100 + advance, 60, 100 + advance, 100, mPaint);


将程序打包成系统应用才能获得系统权限
添加清单文件
android:sharedUserId="android.uid.system"
build未签名apk
android studio –> build –> build apk(s)
找到系统签名秘钥和系统签名工具
系统密钥为:platform.pk8和platform.x509.pem
AOSP路径: build\target\product\security
工具为:signApk.jar
AOSP路径:/out/host/linux-x86/framework/ signApk.jar
对未签名apk进行签名
使用下边的命令进行签名
java -jar $(signApk.jar的全路径地址) $(platform.x509.pem的全路径地址) $(platform.pk8的全路径地址) $(未签名apk文件的全路径地址) $(要生成的apk文件的全路径地址)
放入/system/app/文件夹下(需要root权限)
adb remount
adb push $(系统签名过的apk全路径地址) /system/app/
解决方法:
检查自定义viewgroup的构造方法
public SearchHistoryView(Context context) {
this(context, null);
}
public SearchHistoryView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SearchHistoryView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
看构造方法调用是否正确,参数是否正确。我出现这个错误是在两个参数的构造方法中把第二个参数写成了null导致这个错误。
原因:
在自定义viewgroup的类中,构造方法中一般会调用两个参数的构造方法,在写构造方法时注意,一般都是最后调用this(arg1, arg2,arg3)这个构造方法,如果出现对象为空的情况,检查构造方法,参数是否正确,是否调用正确。
解决方法:
自定义LayoutParams类,
public class LayoutParams extends MarginLayoutParams {
public int gravity = -1;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
gravity = Gravity.TOP;
}
public LayoutParams(int width, int height, int gravity) {
super(width, height);
this.gravity = gravity;
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
并在自定义viewgroup类中重写如下几个构造方法
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return super.checkLayoutParams(p) && p instanceof LayoutParams;
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
原因:
我是按照网上的zhy大神的flowlayout教程写下来的,他是在自定义viewgroup中重写了generatelayoutparams()方法,然后在onmeasure中直接讲layoutparams直接强转成marginlayoutparams,做demo的时候因为是直接在xml中把子view添加到viewgroup控件中,这种做法是没有问题的,但是在写项目的时候因为需要动态添加布局,这时候就出现了以上的问题。建议去看一下layoutparams的知识。
解决方法:
因为我使用的是动态添加子view的方式
TextView textView = (TextView) LayoutInflater.from(SearchActivity.this).inflate(R.layout.item_shv_textview, null, false);
textView.setText(bean);
上边的代码一般都是我写inflat view的代码,3个参数中,arg1代表资源文件,arg2代表父布局,arg3代表是否依赖父布局,一般情况下用到的时候我都是上边的代码,但是在这里用要改为
TextView textView = (TextView) LayoutInflater.from(SearchActivity.this).inflate(R.layout.item_shv_textview, mParent, false);
textView.setText(bean);
就是说必须指定他所在的viewgroup,margin属性才有值.
原因:
通俗点的解释就是将一些零碎的,分散的,相对独立的知识点或者概念加以整合,使之形成具有一定联系的知识系统。

明确知识体系的主题和用途
我的知识体系是什么,我做这个是为了什么。
只有搞清楚自己知识体系的方向,找到自己最愿意学习的部分,并且能让自己更专业的知识,你才有足够的动力去坚持。
明确知识体系的主题和用途可以帮助你迅速理清脉络,剔除无效信息,不必再在无关的内容上浪费自己宝贵的时间。
知识的整理和分类
从广度到深度
可以使用有道云,印象笔记等笔记类软件进行整合,建立专门的笔记本,然后把你的知识点按照名称,作为笔记归纳到你的知识体系中。
知识的输出和运用
把自己学到的知识写成文章等发出去。
“每章小结”是很有必要的。不一定要每章,至少一周一复习
找到获取知识的途径
把握知识动态,更新知识体系
要去经常浏览最新的跟自己知识体系有关的新技术
阅读的愉悦感
一个感兴趣的内容能加强你的抗干扰能力,把碎片化阅读的效率极大的提高。
碎片化信息的第一选择是:具有吸引力的内容
难度较低的陌生材料或者熟悉的材料
碎片化阅读并不盲目追求陌生知识,陌生知识是对自己有用,但是陌生知识一般有自己的知识体系。你可以在遇到陌生领域的时候,先去构建该领域知识体系,了解该领域基本知识之后再进行阅读。
最好把有价值的内容随手保存到笔记软件里,整合在一起。减少出现“我好像在哪里见过这个问题”“不过我想不起在哪里看到的了”的困扰,原作者推荐有道云笔记
有目的的阅读
阅读时可以将有用的片段进行保存,可以保存到云笔记等软件中。
无目的的阅读和浏览
有趣的内容也可以浏览,扩大知识体系
分类是一种重要的科学方法,从碎片到块到系统,归纳可以说是核心的一步。
带着疑问,遇到就保存,清晰的整理归类后,绕开繁琐复杂的信息,大脑的顿悟系统就开始起作用了,你会发现比较复杂的事物变的易于理解和条理清晰起来。
分好类之后,再遇到什么有用内容,我都知道保存归纳到哪里,并且去哪里提取相关的信息。除此之外,我还有很多其他学科类收集与整理,都获益颇多。同理,题主的很多的学习与工作的事都可以通过这个方式提高效率并且对于问题有更好的认识,这也是为建立知识系统进行的必要铺垫。
在你归纳整合之后,你就可以对当前知识进行系统的整理,建立一个文件夹,写一些心得等,基本的知识分类框架就已经出来。
在Android应用层的开发中,使用到wifi相关知识点的地方并不多,所以之前对wifi开发并不熟悉,最近接到的两个硬件项目都有用到wifi并且知识点越来越深入,所以有必要记一下笔记,做一些注释。
Android中要使用系统功能一般都要申请权限,在6.0上可能还要手动申请权限,这里wifi需要的权限有
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> // 需要系统权限 [定位权限]
其中在6.0以上设备,定位权限需要主动申请,并且如果要获取扫描wifi列表需要打开系统的定位开关。
ScanResult类用于存放wifi扫描结果信息,包含ssid(wifi名称), bssid(网络接入点地址),capabilities(加密类型),frequency(传输频率),level(信号强度)等。这里的解释并不太标准,但是对应功能很形象,如果你要了解更多,可以去具体的查阅资料。
WifiConfiguration类用于存放wifi的配置信息。包括wifi的密码,加密类型,网络id(用于连接wifi)等。他的几个子类分别对应秘钥加密方式,安全协议等,这些在设置wifi配置的时候会被用到
wifiinfo类用来描述wifi属性和连接状态。暴露了一些方法给开发者调用。getBSSID(), getMacAddress(), getIpAddress(),getSSID()等
WifiManager类是framework层暴露的api,用来管理wifi。通过调用 Context.getSystemService(Context.WIFI_SERVICE)可以得到类的实例。通过他可以得到:1.已经配置的网络列表。2.当前连接的wifi。3.扫描到的wifi。4.以及一些常量表示广播的意图等
/**
* 判断wifi是否打开
* <p>需添加权限 {@code <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>}</p>
*
* @return {@code true}: 是<br>{@code false}: 否
*/
public static boolean getWifiEnabled() {
@SuppressLint("WifiManagerLeak")
WifiManager wifiManager = (WifiManager) Utils.getApp().getSystemService(Context.WIFI_SERVICE);
return wifiManager.isWifiEnabled();
}
/**
* 打开或关闭wifi
* <p>需添加权限 {@code <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>}</p>
*
* @param enabled {@code true}: 打开<br>{@code false}: 关闭
*/
public static void setWifiEnabled(final boolean enabled) {
@SuppressLint("WifiManagerLeak")
WifiManager wifiManager = (WifiManager) Utils.getApp().getSystemService(Context.WIFI_SERVICE);
if (enabled) {
if (!wifiManager.isWifiEnabled()) {
wifiManager.setWifiEnabled(true);
}
} else {
if (wifiManager.isWifiEnabled()) {
wifiManager.setWifiEnabled(false);
}
}
}
/**
*
* 获取WIFI列表
* <p>需要权限{@code <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>}</p>
* <p>注意Android6.0上需要主动申请定位权限,并且打开定位开关</p>
*
* @param context 上下文
* @return wifi列表
*/
public static List<ScanResult> getWifiList(Context context) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
List<ScanResult> scanResults = wm.getScanResults();
Collections.sort(scanResults, new Comparator<ScanResult>() {
@Override
public int compare(ScanResult scanResult1, ScanResult scanResult2) {
return scanResult2.level - scanResult1.level;
}
});
return scanResultsCopy;
}
/**
* 获取当前链接的WiFi信息
*
* @param context 上下文
* @return 当前wifi数据
*/
public static WifiInfo getCurrentWifi (Context context) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
return wm.getConnectionInfo();
}
public static String getWifiEncryptTypeStr (String capabilitie) {
if (TextUtils.isEmpty(capabilitie)) return null;
String encryptType;
if (capabilitie.contains("WPA") && capabilitie.contains("WPA2")) {
encryptType = "WPA/WPA2 PSK";
} else if (capabilitie.contains("WPA2")) {
encryptType = "WPA2 PSK";
} else if (capabilitie.contains("WPA")) {
encryptType = "WPA PSK";
} else if (capabilitie.contains("WEP")) {
encryptType = "WEP";
} else {
encryptType = "NONE";
}
return encryptType;
}
/**
* wifi加密方式有5种
* 0 - WPA/WPA2 PSK
* 1 - WPA2 PSK
* 2 - WPA PSK
* 3 - WEP
* 4 - NONE
* @param capabilitie
* @return
*/
public static int getWifiEncryptType (String capabilitie) {
if (TextUtils.isEmpty(capabilitie)) return -1;
int encryptType;
if (capabilitie.contains("WPA") && capabilitie.contains("WPA2")) {
encryptType = 0;
} else if (capabilitie.contains("WPA2")) {
encryptType = 1;
} else if (capabilitie.contains("WPA")) {
encryptType = 2;
} else if (capabilitie.contains("WEP")) {
encryptType = 3;
} else {
encryptType = 4;
}
return encryptType;
}
/**
* @des 连接已经保存过配置的wifi
* @param context
* @param ssid
*/
public static void connectWifi (Context context, String ssid) {
Log.d(TAG, "connectWifi: 去连接wifi: " + ssid);
if (!getWifiEnabled()) return;
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiConfiguration wc = new WifiConfiguration();
wc.SSID = "\"" + ssid + "\"";
WifiConfiguration configuration = getWifiConfig(context, ssid);
if (configuration != null) {
wm.enableNetwork(configuration.networkId, true);
}
}
/**
* @des 连接没有配置过的wifi
* @param context
* @param ssid
* @param password
* @param encryptType
*/
public static void connectWifi (Context context, String ssid, String password, int encryptType) {
Log.d(TAG, "connectWifi: 去连接wifi: " + ssid);
if (!getWifiEnabled()) return;
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiConfiguration wc = new WifiConfiguration();
wc.allowedAuthAlgorithms.clear();
wc.allowedGroupCiphers.clear();
wc.allowedKeyManagement.clear();
wc.allowedPairwiseCiphers.clear();
wc.allowedProtocols.clear();
wc.SSID = "\"" + ssid + "\"";
WifiConfiguration configuration = getWifiConfig(context, ssid);
if (configuration != null) {
wm.removeNetwork(configuration.networkId);
}
switch (encryptType) {
case 4://不加密
wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
break;
case 3://wep加密
wc.hiddenSSID = true;
wc.wepKeys[0] = "\"" + password +"\"";
wc.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
break;
case 0: //wpa/wap2加密
case 1: //wpa2加密
case 2: //wpa加密
wc.preSharedKey = "\"" + password + "\"";
wc.hiddenSSID = true;
wc.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
wc.status = WifiConfiguration.Status.ENABLED;
break;
}
int network = wm.addNetwork(wc);
wm.enableNetwork(network, true);
}
public static void disConnectWifi (Context context, int networkId) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
wm.disableNetwork(networkId);
wm.disconnect();
}
/**
* @des 清除wifi配置信息
* @param context
* @param ssid
*/
public static void clearWifiInfo(Context context, String ssid) {
Log.d(TAG, "clearWifiInfo: 清除WIFI配置信息: " + ssid);
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
String newSSID = "\"" + ssid + "\"";
if (!(ssid.startsWith("\"") && ssid.endsWith("\""))) {
newSSID = "\"" + ssid + "\"";
} else {
newSSID = ssid;
}
WifiConfiguration configuration = getWifiConfig(context, newSSID);
configuration.allowedAuthAlgorithms.clear();
configuration.allowedGroupCiphers.clear();
configuration.allowedKeyManagement.clear();
configuration.allowedPairwiseCiphers.clear();
configuration.allowedProtocols.clear();
if (configuration != null) {
wm.removeNetwork(configuration.networkId);
wm.saveConfiguration();
}
}
public static WifiConfiguration getWifiConfig (Context context, String ssid) {
Log.d(TAG, "getWifiConfig: 获取wifi配置信息: " + ssid);
if (TextUtils.isEmpty(ssid)) return null;
String newSSID;
if (!(ssid.startsWith("\"") && ssid.endsWith("\""))) {
newSSID = "\"" + ssid + "\"";
} else {
newSSID = ssid;
}
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
List<WifiConfiguration> configuredNetworks = wm.getConfiguredNetworks();
for (WifiConfiguration configuration : configuredNetworks) {
if (newSSID.equalsIgnoreCase(configuration.SSID)) {
return configuration;
}
}
return null;
}