
《Android群英传》作者
创建后台任务的两种代码模式 后台任务是每个App都需要的一些行为,毕竟主线程是大爷,拖不起,伤不起,脏活累活都只能在不见天日的后台去做。 最简单的后台任务,可以说是直接开一个线程就可以了,或者说来个Service,再开个线程。但这些并不是官方认证的最佳实践,实际上,Google早就考虑到了这一点,并把这些需求进行了封装,给我们提供了非常好的后台任务解决方案,并在Training上进行了讲解: 官网镇楼: https://developer.android.com/training/best-background.html 当然,本文并不是翻译,而是给大家分析两种创建后台任务的基本方法。 模式一:IntentService 这是一个一直被人遗忘的Service,但实际上却是Google一直推荐的后台任务工具类。 IntentService是一个轻量级的Service,系统帮我们自动调用了Service的一些方法,让我们可以一键完成后台任务的创建。 但IntentService与Service还是有所不同的: IntentService运行在独立线程,可以直接执行耗时操作,不会阻塞UI线程 IntentService使用onHandleIntent来处理后台任务,处理完毕后就会自动退出,不用手动退出,并不会常住后台,想动歪脑筋的可以放弃了 IntentService的工作队列是单线程的,也就是说,每次只会操作一个IntentService,多个任务是排队处理的,新任务会等待旧任务的执行完成再执行,正在执行的任务和线程一样,是无法中断的 IntentService本身是单向交互的,默认不存在回调UI线程的接口,这也是IntentService的一个局限,默认只能处理后台任务,但不能更新UI(但实际上可以) 使用IntentService创建后台任务 创建IntentService非常简单,简单到和创建一个类差不多,但要注意,必须实现无参构造方法,并实现OnHandleIntent(Intent intent)方法,该方法自动在新线程执行,并通过,代码如下: public class MyBackgroundTaskIntentService extends IntentService { public MyBackgroundTaskIntentService() { super("MyBackgroundTaskIntentService"); } @Override protected void onHandleIntent(Intent intent) { // BackgroundTask } } 启动IntentService: Intent backgroundTask = new Intent(this, MyBackgroundTaskIntentService.class); startService(backgroundTask); 不同的任务可以通过Intent中设置Data来进行区分来进行区分。 我们通过startService来启动IntentService,但是又要注意的是,IntentService在第一次调用startService时创建服务,如果在IntentService还没有完成后台任务时,再次调用了startService,那么不再创建服务,而是在任务队列添加一个任务,实际上就是将执行内容添加到了执行队列,等待执行,当队列内所有任务都执行完毕后,Service自动销毁。 IntentService任务回源 前面说了,IntentService没有任务回调,也就是说,Activity启动了IntentService执行一个后台任务,当IntentService执行完毕后,却不能收到回执,无法更加后台执行结果就行下一步操作。所以,这个时候,我们需要使用广播来进行任务的回源操作。 @Override protected void onHandleIntent(Intent intent) { String data = intent.getDataString(); // Do something Intent localTask = new Intent(COM_XYS_MY_LOCAL_BROADCAST); localTask.putExtra("status", "status"); LocalBroadcastManager.getInstance(this).sendBroadcast(localTask); LocalBroadcastManager.getInstance(this).sendBroadcast(localTask); } 这里我们使用本地广播,而不是一般的全局广播来进行消息的处理,原因主要是因为LocalBroadcast比普通广播更加安全,同时效率更高。 public class MyBackgroundTaskReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String status = intent.getStringExtra("status"); } } 记得LocalBroadcast需要进行动态注册和释放: IntentFilter intentFilter = new IntentFilter(COM_XYS_MY_LOCAL_BROADCAST); MyBackgroundTaskReceiver receiver = new MyBackgroundTaskReceiver(); LocalBroadcastManager.getInstance(this).registerReceiver(receiver, intentFilter); 那么通过这种方式,我们就可以很方便的实现IntentService的后台任务处理,同时完成任务执行完毕后的回源更新。 实际上,在AndroidStudio中创建一个IntentService,AS自动就会帮我们创建好这样的模板代码: 创建好的代码如下: 模式二:Loader Loader是Android提供的解决后台异步任务处理的利器,但是感觉很少有能够在全线铺开使用的,Loader模式可以让异步处理变的非常轻松。 使用Loader的一个非常好的优势,就是不用自己来管理后台任务的状态了,全部交给系统来进行托管。 官网镇楼: https://developer.android.com/reference/android/content/AsyncTaskLoader.html 创建Loader 我们以AsyncTaskLoader为例,其它的Loader也类似: public class MyBackgroundLoader extends AsyncTaskLoader<String> { public MyBackgroundLoader(Context context) { super(context); onContentChanged(); } @Override protected void onStartLoading() { super.onStartLoading(); if (takeContentChanged()) { forceLoad(); } } @Override public String loadInBackground() { return "status"; } } AsyncTaskLoader与其它类型的Loader稍有不同,AsyncTaskLoader必须要在onStartLoading中执行forceLoad方法,否则不会生效,所以,官网上建议AsyncTaskLoader使用上面的代码模板进行创建。 我们在loadInBackground方法中,进行后台任务的执行。 执行Loader 使用Loader一般需要实现LoaderManager.LoaderCallbacks接口,并完成它的几个回调方法: public class LoaderActivity extends Activity implements LoaderManager.LoaderCallbacks<String> { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.loader); } public void initLoader(View view) { getLoaderManager().initLoader(0, null, this); } @Override public Loader<String> onCreateLoader(int id, Bundle args) { return new MyBackgroundLoader(this); } @Override public void onLoadFinished(Loader<String> loader, String data) { } @Override public void onLoaderReset(Loader<String> loader) { } } 通过initLoader,我们对Loader进行初始化,并在onCreateLoader中返回具体要执行的Loader,Loader会自动调用指定Loader的loadInBackground方法,在loadInBackground执行完毕后,会回调onLoadFinished方法,从而完成一次异步任务的处理和回源。 番外篇:WakefulBroadcastReceiver 我们还要另外讲一个后台处理的特殊类——WakefulBroadcastReceiver,这个类用来处理需要申请WakeLock的特殊后台服务,通过WakefulBroadcastReceiver,我们可以避免自己手动来管理WakeLock,将锅甩给系统。 我们创建一个MyWakefulBroadcastReceiver: public class MyWakefulBroadcastReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, MyBackgroundTaskIntentService.class); startWakefulService(context, service); } } 在这个MyWakefulBroadcastReceiver中,我们将一个需要申请WakeLock的后台任务与MyWakefulBroadcastReceiver绑定,并调用startWakefulService来启用这个IntentService。 那么在 public class MyBackgroundTaskIntentService extends IntentService { public MyBackgroundTaskIntentService() { super("MyBackgroundTaskIntentService"); } @Override protected void onHandleIntent(Intent intent) { // Background Task MyWakefulBroadcastReceiver.completeWakefulIntent(intent); } } 在后台IntentService中,完成Task后,只需要使用MyWakefulBroadcastReceiver.completeWakefulIntent来结束这个任务,即可释放WakeLock。 通过这种方式来执行后台任务,可以让需要申请WakeLock的后台任务更加安全的的执行。 总结 后台任务是一个Android App不可或缺的组成部分,同时也是影响系统性能的一个重要部分,大家不能因为看不见,就对它置之不理,我们需要对后台任务进行最佳实践,更加好的优化App后台的处理性能。 当然,不管是IntentService还是Loader,都是处理后台任务的最基础的方法,以IntentService来说,它是一个单消息队列,因此,对一些高密度、高并发的后台任务就不太适合,我们需要使用线程池来进行手动的管理。后台任务的最佳实践是一个持续的过程,需要开发者针对使用场景进行不断的优化。
前言 本文专门写给那些想在限购地区买房,又担心跳槽会影响买房资格的开发者,一篇文章了解『跳槽对限购资格的影响,到底是杞人忧天,还是危机四伏』 首先我们来了解下现在买房的限购条件(以下均是外地户籍,本地户籍,你可以看看其它技术文章),我们以上海为例: 结婚 && (社保连续5年 || 个税连续5年) 我们可以发现,实际上最困难的地方,就是这个5年连续的社保或者个税,结婚毕竟只要9块钱,可这个社保、个税,可是要5年而且不断的(当然,上海这边有些区的房地产交易中心是允许63个月满60个月即可的,但完全看各个交易中心的执行标准)。 那么现在问题来了,有多少人应届毕业生能在一家公司待满5年的?如果中途跳槽,会不会导致社保或者个税断掉呢? 社保 我们先来看比较简单的社保,社保,是每个月发工资的时候,公司和个人都会缴纳的一定比例的社会保险,也就是说,只要公司每个月给你发工资了,就都会给你缴纳当月的社保,换句话说,即使你跳槽,但只要中间没有最多休息超过一个月,就不用担心社保会断,一般来说,当月15号之前离职,都由离职的那家公司给你缴纳当月的社保,但如果你中途休息的时间超过一个月,那么就势必会出现一个月的空档期,上下家公司都没有给你缴纳社保,这就会让你丧失购房资格。 这只是一个最基本的注意点,毕竟如果你考虑要买房的话,只要跳槽的时候能衔接上上下家公司即可,但仅仅你自己注意还是不够的,一些小公司,很可能给你找各种借口,给你入职的时候晚交社保,特别是一些应届生入职的时候,毕竟少交一个月,能省不少钱啊,而且,如果当月公司没有给你缴纳社保,后面被你查到了,公司也给你补缴了社保,但是,你依然失去了购房资格,所以,应届生在入职的时候,一定要督促公司严格缴纳社保,不然你后面买房的时候,很可能就因为这一个月、二个月而后悔不已。 个税 现在看来,跳槽对社保的影响还是比较小的,那么个税呢? 个税与社保不同,社保是按月算,只有201701这样的时间统计,但是个税不一样,个税有两个时间,一个叫『个税所得期』,一个叫『个税所属期』,我不是专业学税务的,所以我只能用比较通俗的语言给大家解释下,比如你的公司,2月10日发一月份的工资,那么『个税所得期是201701』,『个税所属期是201702』,个税所属期,实际上就是实际缴纳个税的月份,你是几月份缴纳,那么所属期就是几月份。 在了解了这个概念之后,我们就可以入坑了。 房地产交易中心所认的个税连续,是按照个税所属期连续来判断的,这样问题就来了,看到这里,不知道有多少人想到了。举个栗子,假如你上家公司是当月发当月的工资,例如1月31日,最后一天发1月的工资,那么你离职前,最后一次个税所属期就是201701,你入职之后,如果新公司是当月发上月的工资,例如2月10日发1月份的工资,那么你的2月份工资,实际上到3月10日才会给你发,但这个时候,你新公司的个税所属期,已经是201703了!但实际上并没有错,因为个税所属期是按照缴税的时间来算的,虽然你一天工作时间都没断,但却真真切切的失去了使用个税的购房资格。 所以这一点,需要所有准备跳槽的开发者关注,如果你跳槽的公司和现在的公司,都是当月发工资,或者都是次月发工资,那么个税就不会因为跳槽而中断,但只要上下家工资发放时间不同,就会导致中断,而且补缴无效。 总结 所以,从上面的分析来看,跳槽,确实是有可能导致失去买房资格的。跳槽的人需要在跳槽时,与新老公司的人好好了解下社保个税的转移时间点,才能尽可能的保证不失去购房资格。 当然,上面分析的也是单独针对个税和社保来说的,实际上只要二者满足一个就可以了,同时,你和你老婆,也只要有一个人满足即可,所以说,如果有比较合适的职位,在确定总体情况下不会影响到购房资格,跳槽当然也是可以的。 对于购房来说,跳槽到底是杞人忧天还是危机四伏,你看明白了吗?
PathInterpolator 在v4 support library:Revision 22.1.0的时候,Google在兼容库中增加了几个新的类,用于创建更加真实的动画效果。 Added the following interpolation classes for animation: FastOutLinearInInterpolator, FastOutSlowInInterpolator, LinearOutSlowInInterpolator, LinearOutSlowInInterpolator, and PathInterpolatorCompat. 从命名我们大致可以看出来,这个实际上就是新增的插值器,但实现了更加真实的动画效果,了解我之前关于插值器的文章的朋友,应该很清楚,不了解的开发者可以先看下关于插值器的介绍:模拟自然动画的精髓——https://gold.xitu.io/post/57e33e2cc4c971005f4bf6ff PathInterpolatorCompat 其它几个Interpolator非常好理解,实际上在没有他们之前,我们也可以通过自己来计算函数值来创建这样的Interpolator,也就是类似——缓进急出、缓出急进这样的插值器效果。 那么今天我们的主角,就是——PathInterpolatorCompat,他实际上是PathInterpolator的兼容版本,可以兼容到Android的低版本设备。利用PathInterpolatorCompat,我们可以非常方便的创建二阶、三阶的贝塞尔曲线动画Interpolator。 官网镇楼 https://developer.android.com/reference/android/support/v4/view/animation/PathInterpolatorCompat.html 这个类的使用非常简单,只有一个重载的creat()方法。 Method code create(Path path) Create an Interpolator for an arbitrary Path. create(float controlX1, float controlY1, float controlX2, float controlY2) Create an Interpolator for a cubic Bezier curve. create(float controlX, float controlY) Create an Interpolator for a quadratic Bezier curve. 当然,不仅仅是贝塞尔曲线,实际上只要是Path绘制的曲线,都可以作用在PathInterpolatorCompat上。 OK,有了这个工具,我们就可以很方便的使用它来创建各种插值曲线了,举个非常简单的例子: Path path = new Path(); path.cubicTo(0.2f, 0f, 0.1f, 1f, 0.5f, 1f); path.lineTo(1f, 1f); ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 500); animator.setInterpolator(PathInterpolatorCompat.create(path)); animator.start(); 我们绘制了一个简单的三阶贝塞尔曲线,并作用到PathInterpolatorCompat设置给Animation,这样就完成了,不再需要像我们之前做的那样,通过二阶、三阶贝塞尔曲线的数学计算公式来进行计算,极大的方便了开发者。
什么是AOP AOP是Aspect Oriented Programming的缩写,即『面向切面编程』。它和我们平时接触到的OOP都是编程的不同思想,OOP,即『面向对象编程』,它提倡的是将功能模块化,对象化,而AOP的思想,则不太一样,它提倡的是针对同一类问题的统一处理,当然,我们在实际编程过程中,不可能单纯的安装AOP或者OOP的思想来编程,很多时候,可能会混合多种编程思想,大家也不必要纠结该使用哪种思想,取百家之长,才是正道。 那么AOP这种编程思想有什么用呢,一般来说,主要用于不想侵入原有代码的场景中,例如SDK需要无侵入的在宿主中插入一些代码,做日志埋点、性能监控、动态权限控制、甚至是代码调试等等。 AspectJ AspectJ实际上是对AOP编程思想的一个实践,当然,除了AspectJ以外,还有很多其它的AOP实现,例如ASMDex,但目前最好、最方便的,依然是AspectJ。 在Android项目中使用AspectJ AOP的用处非常广,从Spring到Android,各个地方都有使用,特别是在后端,Spring中已经使用的非常方便了,而且功能非常强大,但是在Android中,AspectJ的实现是略阉割的版本,并不是所有功能都支持,但对于一般的客户端开发来说,已经完全足够用了。 在Android上集成AspectJ实际上是比较复杂的,不是一句话就能compile,但是,鄙司已经给大家把这个问题解决了,大家现在直接使用这个SDK就可以很方便的在Android Studio中使用AspectJ了。Github地址如下: https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx 另外一个比较成功的使用AOP的库是Jake大神的Hugo: https://github.com/JakeWharton/hugo 接入说明 首先,需要在项目根目录的build.gradle中增加依赖: classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8' 完整代码如下: buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.0-beta2' classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } 然后再主项目或者库的build.gradle中增加AspectJ的依赖: compile 'org.aspectj:aspectjrt:1.8.9' 同时在build.gradle中加入AspectJX模块: apply plugin: 'android-aspectjx' 这样就把整个Android Studio中的AspectJ的环境配置完毕了,如果在编译的时候,遇到一些『can’t determine superclass of missing type xxxxx』这样的错误,请参考项目README中关于excludeJarFilter的使用。 aspectjx { //includes the libs that you want to weave includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library' //excludes the libs that you don't want to weave excludeJarFilter 'universal-image-loader' } AspectJ入门 我们通过一段简单的代码来了解下基本的使用方法和功能,新建一个AspectTest类文件,代码如下: @Aspect public class AspectTest { private static final String TAG = "xuyisheng"; @Before("execution(* android.app.Activity.on**(..))") public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodBefore: " + key); } } 在类的最开始,我们使用@Aspect注解来定义这样一个AspectJ文件,编译器在编译的时候,就会自动去解析,并不需要主动去调用AspectJ类里面的代码。 我的原始代码很简单: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } 通过这种方式编译后,我们来看下生成的代码是怎样的。AspectJ的原理实际上是在编译的时候,根据一定的规则解析,然后插入一些代码,通过aspectjx生成的代码,会在Build目录下: 通过反编译工具查看下生成内容: 我们可以发现,在onCreate的最前面,插入了一行AspectJ的代码。这个就是AspectJ的主要功能,抛开AOP的思想来说,我们想做的,实际上就是『在不侵入原有代码的基础上,增加新的代码』。 AspectJ之Join Points Join Points,简称JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整个执行过程切成了一段段不同的部分。例如,构造方法调用、调用方法、方法执行、异常等等,这些都是Join Points,实际上,也就是你想把新的代码插在程序的哪个地方,是插在构造方法中,还是插在某个方法调用前,或者是插在某个方法中,这个地方就是Join Points,当然,不是所有地方都能给你插的,只有能插的地方,才叫Join Points。 AspectJ之Pointcuts Join Points和Pointcuts的区别实际上很难说,我也不敢说我理解的一定对,但这些都是概念上的内容,并不影响我们去使用。 Pointcuts,在我理解,实际上就是在Join Points中通过一定条件选择出我们所需要的Join Points,所以说,Pointcuts,也就是带条件的Join Points,作为我们需要的代码切入点。 AspectJ之Advice 又来一个Advice,Advice其实是最好理解的,也就是我们具体插入的代码,以及如何插入这些代码。我们最开始举的那个例子,里面就是使用的最简单的Advice——Before。类似的还有After、Around,我们后面来讲讲他们的区别。 AspectJ之切点语法 我们以前面的Demo来看下最简单的AspectJ语法: @Before("execution(* android.app.Activity.on**(..))") public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable { } 这里会分成几个部分,我们依次来看: @Before:Advice,也就是具体的插入点 execution:处理Join Point的类型,例如call、execution (* android.app.Activity.on**(..)):这个是最重要的表达式,第一个『*』表示返回值,『*』表示返回值为任意类型,后面这个就是典型的包名路径,其中可以包含『*』来进行通配,几个『*』没区别。同时,这里可以通过『&&、||、!』来进行条件组合。()代表这个方法的参数,你可以指定类型,例如android.os.Bundle,或者(..)这样来代表任意类型、任意个数的参数。 public void onActivityMethodBefore:实际切入的代码。 这里还有一些匹配规则,可以作为示例来进行讲解: 表达式 含义 java.lang.String 匹配String类型 java.*.String 匹配java包下的任何“一级子包”下的String类型,如匹配java.lang.String,但不匹配java.lang.ss.String java..* 匹配java包及任何子包下的任何类型,如匹配java.lang.String、java.lang.annotation.Annotation java.lang.*ing 匹配任何java.lang包下的以ing结尾的类型 java.lang.Number+ 匹配java.lang包下的任何Number的自类型,如匹配java.lang.Integer,也匹配java.math.BigInteger 参数 含义 () 表示方法没有任何参数 (..) 表示匹配接受任意个参数的方法 (..,java.lang.String) 表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法 (java.lang.String,..) 表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法 (*,java.lang.String) 表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法 AspectJ实例 Before、After 这两个Advice应该是使用的最多的,所以,我们先来看下这两个Advice的实例,首先看下Before和After。 @Before("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))") public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodBefore: " + key); } @After("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))") public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodAfter: " + key); } 经过上面的语法解释,现在看这个应该很好理解了,我们来看下编译后的类: 我们可以看见,在原始代码的基础上,增加了Before和After的代码,Log也能被正确的插入并打印出来。 Around Before和After其实还是很好理解的,也就是在Pointcuts之前和之后,插入代码,那么Around呢,从字面含义上来讲,也就是在方法前后各插入代码,是的,他包含了Before和After的全部功能,代码如下: @Around("execution(* com.xys.aspectjxdemo.MainActivity.testAOP())") public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { String key = proceedingJoinPoint.getSignature().toString(); Log.d(TAG, "onActivityMethodAroundFirst: " + key); proceedingJoinPoint.proceed(); Log.d(TAG, "onActivityMethodAroundSecond: " + key); } 其中,proceedingJoinPoint.proceed()代表执行原始的方法,在这之前、之后,都可以进行各种逻辑处理。 原始代码: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP(); } public void testAOP() { Log.d("xuyisheng", "testAOP"); } } 我们先来看下编译后的代码: 我们可以发现,Around确实实现了Before和After的功能,但是要注意的是,Around和After是不能同时作用在同一个方法上的,会产生重复切入的问题。 自定义Pointcuts 自定义Pointcuts可以让我们更加精确的切入一个或多个指定的切入点。 首先,我们需要自定义一个注解类,例如——DebugTool.java: /** * 自定义AOP注解 * <p> * Created by xuyisheng on 17/1/12. */ @Retention(RetentionPolicy.CLASS) @Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) public @interface DebugTool { } 然后在需要插入代码的地方使用这个注解: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP(); } @DebugTool public void testAOP() { Log.d("xuyisheng", "testAOP"); } } 最后,我们来创建自己的切入文件。 @Pointcut("execution(@com.xys.aspectjxdemo.DebugTool * *(..))") public void DebugToolMethod() { } @Before("DebugToolMethod()") public void onDebugToolMethodBefore(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onDebugToolMethodBefore: " + key); } 先定义Pointcut,并申明要监控的方法名,最后,在Before或者其它Advice里面添加切入代码,即可完成切入。 编译好的代码如下: 通过这种方式,我们可以非常方便的监控指定的Pointcut,从而增加监控的粒度。 call和execution 在AspectJ的切入点表达式中,我们前面都是使用的execution,实际上,还有一种类型——call,那么这两种语法有什么区别呢,我们来试验下就知道了。 被切代码依然很简单: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP(); } public void testAOP() { Log.d("xuyisheng", "testAOP"); } } 先来看execution,代码如下: @Before("execution(* com.xys.aspectjxdemo.MainActivity.testAOP(..))") public void methodAOPTest(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "methodAOPTest: " + key); } 编译之后的代码如下所示: 再来看下call,代码如下: @Before("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))") public void methodAOPTest(JoinPoint joinPoint) throws Throwable { String key = joinPoint.getSignature().toString(); Log.d(TAG, "methodAOPTest: " + key); } 编译之后的代码如下所示: 其实对照起来看就一目了然了,execution是在被切入的方法中,call是在调用被切入的方法前或者后。 对于Call来说: Call(Before) Pointcut{ Pointcut Method } Call(After) 对于Execution来说: Pointcut{ execution(Before) Pointcut Method execution(After) } 切入点过滤与withincode 除了前面提到的call和execution,比较常用的还有一个withincode。这个语法通常来进行一些切入点条件的过滤,作更加精确的切入控制。我们可以参考下面这个例子: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); testAOP1(); testAOP2(); } public void testAOP() { Log.d("xuyisheng", "testAOP"); } public void testAOP1() { testAOP(); } public void testAOP2() { testAOP(); } } testAOP1()和testAOP2()都调用了testAOP()方法,但是,现在想在testAOP2()方法调用testAOP()方法的时候,才切入代码,那么这个时候,就需要使用到Pointcut和withincode组合的方式,来精确定位切入点。 // 在testAOP2()方法内 @Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))") public void invokeAOP2() { } // 调用testAOP()方法的时候 @Pointcut("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))") public void invokeAOP() { } // 同时满足前面的条件,即在testAOP2()方法内调用testAOP()方法的时候才切入 @Pointcut("invokeAOP() && invokeAOP2()") public void invokeAOPOnlyInAOP2() { } @Before("invokeAOPOnlyInAOP2()") public void beforeInvokeAOPOnlyInAOP2(JoinPoint joinPoint) { String key = joinPoint.getSignature().toString(); Log.d(TAG, "onDebugToolMethodBefore: " + key); } 我们再来看下编译后的代码: 我们可以看见,只有在testAOP2()方法中被插入了代码,这就做到了精确条件的插入。 异常处理AfterThrowing AfterThrowing是一个比较少见的Advice,他用于处理程序中未处理的异常,记住,这点很重要,是未处理的异常,具体原因,我们等会看反编译出来的代码就知道了。我们随手写一个异常,代码如下: public void testAOP() { View view = null; view.animate(); } 然后使用AfterThrowing来进行AOP代码的编写: @AfterThrowing(pointcut = "execution(* com.xys.aspectjxdemo.*.*(..))", throwing = "exception") public void catchExceptionMethod(Exception exception) { String message = exception.toString(); Log.d(TAG, "catchExceptionMethod: " + message); } 这段代码很简单,同样是使用我们前面类似的表达式,但是这里是为了处理异常,所以,使用了*.*来进行通配,在异常中,我们执行一行日志,编译好的代码如下: 我们可以看见com.xys.aspectjxdemo包下的所有方法都被加上了try catch,同时,在catch中,被插入了我们切入的代码,但是最后,他依然会throw e,也就是说,这个异常已经会被抛出去,崩溃依旧是会发生的。同时,如果你的原始代码中已经try catch了,那么同样也无法处理,具体原因,我们看一个反编译的代码: 可以看见,实际上,原始代码的catch中,又被套了一层try catch,所以,e.printStackTrace()被try catch,也就不会再有异常发生了,也就无法切入了。 AspectJX使用案例 目前鄙司的很多项目都已经使用了这套AOP方案,例如基于AOP的动态权限管理、基于AOP的业务数据埋点、基于AOP的性能监测系统等等。 现在已经开源了一部分基于AOP的动态权限管理的源码,但由于需要剥离业务代码,所以后面会更加完善这功能代码,大家可以继续关注,github地址如下所示: https://github.com/firefly1126/android_permission_aspectjx 其它的AOP项目陆续开源中,大家可以持续关注~ 欢迎关注我的微信公众号
微信Mars——xlog使用全解析 如约而至,微信在12月19日开源了底层的通信库——Mars,其中有一个部分,是一个高性能的日志模块——xlog。 xlog的详细介绍,大家可以参考微信技术公众号的这篇文章——微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog。 本篇文章将带领大家将xlog模块抽取出来,作为一个单独的模块来使用。 编译so库 首先,我们clone下Mars的源码,然后进入其中的libraries目录,直接执行下面的Python脚本: python build_android.py 注意,这里需要配置好本地的NDK编译环境,这里不赘述 Enter menu: 1. build mars static libs. 2. build mars shared libs. 3. build xlog static libs. 4. build xlog shared libs. 5. exit. 我们需要编译两个库:3和4。 编译好之后,就会生成下面的文件: 我们需要的就是里面的Java文件和so库,将mars_android_sdk/src目录下的Java文件以及 libs/复制到你的项目中: 如图所示,工程的配置就完成了。 使用 权限 xlog可以加密每一行输出的文件并写入文件,所以需要下面的权限: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 加载so xlog需要使用到我们前面编译出的两个so库: System.loadLibrary("stlport_shared"); System.loadLibrary("marsxlog"); 初始化 在代码中对xlog进行初始化: final String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath(); final String logPath = SDCARD + "/marssample/log"; //init xlog if (BuildConfig.DEBUG) { Xlog.appenderOpen(Xlog.LEVEL_DEBUG, Xlog.AppednerModeAsync, "", logPath, "MarsSample"); Xlog.setConsoleLogOpen(true); } else { Xlog.appenderOpen(Xlog.LEVEL_INFO, Xlog.AppednerModeAsync, "", logPath, "MarsSample"); Xlog.setConsoleLogOpen(false); } Log.setLogImp(new Xlog()); 使用 使用xlog下的Log类就可以打Log了,跟使用Android原生的Log方式基本一样: import com.tencent.mars.xlog.Log; Log.d("xys", "xysxysxys"); 停止Log记录 在Application或者Activity的销毁方法中,进行xlog的关闭操作,从而生成日志文件: Log.appenderClose(); 解析Log Log生成完毕后,会在指定的路径下生成相应的日志文件: shell@R7:/sdcard/marssample/log $ ll -rw-rw---- root sdcard_r 153600 2016-12-30 17:06 MarsSample.mmap2 -rw-rw---- root sdcard_r 29633 2016-12-30 17:06 MarsSample_20161230.xlog 其中MarsSample.mmap2是缓存文件,不用关心,我们需要的是.xlog文件,我们把这个文件pull出来,使用Mars提供的Python脚本进行解密。 找到Mars源码log/crypt/decode_mars_log_file.py下的这个文件,执行: mars_xlog_sdk python decode_mars_log_file.py ~/Downloads/log/MarsSample_20161230.xlog 即可生成对应的log文件,用Sublime即可打开: 相关内容 大部分的内容实际上都在Mars源码的wiki中,但是内容比较散,所以我这里做了一个比较通用的Guide。 https://github.com/Tencent/mars/wiki/Mars-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98 https://github.com/Tencent/mars/wiki/Mars-Android-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 优势在哪 很多人可能会说,这玩意儿跟原生Log系统,还有log4j这类的日志系统,强大在哪呢?实际上,xlog的优势主要有以下几点: 效率、效率、效率:这是最重要的,通过C层去写日志 低内存、低CPU:性能优势大,不占内存CPU 功能丰富:与原生Log使用几乎一致,但增加了写入文件功能,同时自带加密 更多内容请关注我的微信公众号
动态更换应用Icon 产品:我们可以动态更换App在Launcher里面的Icon吗 开发:不可以 产品:我们可以动态更换App在Launcher里面的Icon吗 开发:不可以 产品:我们可以动态更换App在Launcher里面的Icon吗 开发:不可以 产品:我们可以动态更换App在Launcher里面的Icon吗 开发:让我想想…… 原理1——activity-alias 在AndroidMainifest中,有两个属性: // 决定应用程序最先启动的Activity android.intent.action.MAIN // 决定应用程序是否显示在程序列表里 android.intent.category.LAUNCHER 另外,还有一个activity-alias属性,这个属性可以用于创建多个不同的入口,相信做过系统Setting和Launcher开发的开发者在系统的源码中应该见过很多。 原理2——PM.setComponentEnabledSetting PackageManager是一个大统领类,可以管理所有的系统组件,当然,如果Root了,你还可以管理其它App的所有组件,一些系统优化工具就是通过这个方式来禁用一些后台Service的。 使用方式异常简单: private void enableComponent(ComponentName componentName) { mPm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } private void disableComponent(ComponentName componentName) { mPm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } 根据PackageManager.COMPONENT_ENABLED_STATE_ENABLED和PackageManager.COMPONENT_ENABLED_STATE_DISABLED这两个标志量和对应的ComponentName,就可以控制一个组件的是否启用。 动态换Icon 有了上面的两个原理,来实现动态更换Icon就只剩下思路问题了。 首先,我们创建一个Activity,作为默认的入口并带着默认的图片,再创建一个双11的activity-alias,指向默认的Activity并带有双11的图片,再创建一个双12的activity-alias,指向默认的Activity并带有双12的图片……等等等。 <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity-alias android:name=".Test11" android:enabled="false" android:icon="@drawable/s11" android:label="双11" android:targetActivity=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity-alias> <activity-alias android:name=".Test12" android:enabled="false" android:icon="@drawable/s12" android:label="双12" android:targetActivity=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity-alias> 等等,这样有个问题,那就是这样会在Launcher上显示3个入口,所以,默认我们会把这些activity-alias先禁用,等到要用的时候再启用,养兵千日,用兵一时。 public class MainActivity extends AppCompatActivity { private ComponentName mDefault; private ComponentName mDouble11; private ComponentName mDouble12; private PackageManager mPm; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mDefault = getComponentName(); mDouble11 = new ComponentName( getBaseContext(), "com.xys.changeicon.Test11"); mDouble12 = new ComponentName( getBaseContext(), "com.xys.changeicon.Test12"); mPm = getApplicationContext().getPackageManager(); } public void changeIcon11(View view) { disableComponent(mDefault); disableComponent(mDouble12); enableComponent(mDouble11); } public void changeIcon12(View view) { disableComponent(mDefault); disableComponent(mDouble11); enableComponent(mDouble12); } private void enableComponent(ComponentName componentName) { mPm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } private void disableComponent(ComponentName componentName) { mPm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } } OK了,禁用默认的Activity后,启用双11的activity-alias,结果不变还是指向了默认的Activity,但图标已经发生了改变。 根据ROM的不同,在禁用了组件之后,会等一会,Launcher会自动刷新图标。 效果参考下图。
AccessibilityService从入门到出轨 AccessibilityService根据官方的介绍,是指开发者通过增加类似contentDescription的属性,从而在不修改代码的情况下,让残障人士能够获得使用体验的优化,大家可以打开AccessibilityService来试一下,点击区域,可以有语音或者触摸的提示,帮助残障人士使用App。 当然,现在AccessibilityService已经基本偏离了它设计的初衷,至少在国内是这样,越来越多的App借用AccessibilityService来实现了一些其它功能,甚至是灰色产品。 使用入门 老规矩,官网镇楼 https://developer.android.com/guide/topics/ui/accessibility/services.html https://developer.android.com/training/accessibility/service.html 要使用AccessibilityService实际上非常简单,一般来说,只需要以下三步即可。 继承系统AccessibilityService public class MyAccessibility extends AccessibilityService { private static final String TAG = "xys"; @Override public void onAccessibilityEvent(AccessibilityEvent event) { Log.d(TAG, "onAccessibilityEvent: " + event.toString()); } @Override public void onInterrupt() { } } 其中有两个必须实现的方法:onAccessibilityEvent和onInterrupt。 在onAccessibilityEvent中,我们可以接收所监听的事件。不熟悉这些事件的话,只需要使用toString把这些信息打出来,自己多看几个Log,就大概能够了解了。 新建配置文件 在资源目录res下新建xml文件夹,新建accessibility.xml文件,写入: <?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackSpoken" android:canRetrieveWindowContent="true" android:notificationTimeout="1000"/> 里面有一些比较简单的配置。 其中 description 为 用户允许应用的辅助功能的说明字符串,这里没有指定所要辅助的应用packageNames,当没有指定时,默认辅助所有的应用,建议大家在使用时,指定需要监听的包名(你可以通过|来进行分隔),而不是所有的包名。typeAllMask是设置响应事件的类型,feedbackGeneric是设置回馈给用户的方式,有语音播出和振动。 注册 在AndroidMainifest中注册: <service android:name=".MyAccessibility" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService"/> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility"/> </service> 完成以上步骤后,一个AccessibilityService就可以使用了,你要知道的是,AccessibilityService具有很高的系统权限,所以,系统不会让App直接设置是否启用,需要用户进入设置-辅助功能中去手动启用,这样在一定程度上,保护了用户数据的安全。 如何理解AccessibilityService 很多人可能对AccessibilityService了解的不是很深入,所以认为AccessibilityService是在调用一些系统服务来自动执行一些操作,实际上,这个理解不能算错,当然也不全对,我觉得你可以把AccessibilityService理解为——『按键精灵』。相信很多开发者都玩过PC上的这款软件,他的作用,就是将你一次操作的整个记录,录制下来,然后就可以根据这个记录,重复的执行这些操作,例如:先点击某个输入框,再输入XXXX,再输入验证码,最后点击某按钮,这些操作如果需要重复执行,那么显然是一套机械的步骤,那么通过按键精灵,记录下这些操作后,直接通过脚本就可以完成这些操作。其实AccessibilityService跟这个是一样的,我们记录的,实际上就是我们的操作步骤,或者称之为『脚本』,那么系统在监控整个手机的各种AccessibilityService事件时,就会根据我们的逻辑来判断该使用哪一个脚本。 因此,我们完全可以抽象出一个基类AccessibilityService,并抽象出一些脚本的事件,例如,根据Text查找对应的View、点击某个View、滑动、返回等等,所以,我在这里封装了一个BaseAccessibilityService,这里就不贴具体的代码了,大家可以参考我的Github: https://github.com/xuyisheng/AccessibilityUtil 入门 不知道从什么时候开始,AccessibilityService突然从一个残障人士使用的辅助服务,一跃变成了各种App的黑科技,利用AccessibilityService来做的事情,也越来越偏离了AccessibilityService设计的初衷,各种安全问题也随之暴露出来,Google的理想是好的,愿天下都是安分守己的程序员。 免Root自动安装 这个也许是能考证的最早利用AccessibilityService的使用场景了,最早在一些应用市场中出现,例如用户一次下载了很多App,那么每个App下载完毕后都会弹出安装界面,而且需要用户手动去处理,确实体验不太好,所以后来就出现了利用Root权限来静默安装App的功能,但现在普通用户Root的需求越来越少,所以,AccessibilityService来实现免Root自动安装的黑科技,才走上了桌面。 那么按照我们前面的思路,要实现自动安装,实际上就是把手动安装的步骤脚本化。一般来说,我们要安装一个App,会通过以下几个步骤: 调用系统的安装Intent 在安装界面上寻找『安装』、『下一步』这些操作按钮 点击『安装』、『下一步』按钮 完成安装 那么这些流程化的操作,我们就完全可以通过脚本来实现,下面就是一些简单的代码实现。 调用系统安装Intent: public void autoInstall(View view) { String apkPath = Environment.getExternalStorageDirectory() + "/test.apk"; Uri uri = Uri.fromFile(new File(apkPath)); Intent localIntent = new Intent(Intent.ACTION_VIEW); localIntent.setDataAndType(uri, "application/vnd.android.package-archive"); startActivity(localIntent); } 监控安装界面,并根据逻辑处理点击: @Override public void onAccessibilityEvent(AccessibilityEvent event) { super.onAccessibilityEvent(event); if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.getPackageName().equals("com.android.packageinstaller")) { AccessibilityNodeInfo nodeInfo = findViewByText("安装", true); if (nodeInfo != null) { performViewClick(nodeInfo); } } } 代码写完才发现,看似很牛逼的自动安装,其实不过十几行代码。唯一复杂的,就是抽象化这些流程了。 抢红包 抢红包应该是AccessibilityService火起来的最大因素。网上借助AccessibilityService来实现的抢红包插件也是数不胜数,又是一个看上去很牛逼的功能。那么我们再来分析下,你是怎么抢红包的。 加入你现在在桌面,怎么知道有红包了呢?哦,看通知栏,出现了『微信红包』这几个关键字,然后,你点击这条通知进去,点击红包的那条消息,然后再点击拆红包的按钮,返回,回到桌面。 这样一看,抢红包完全是一个体力活啊,如果有个机器人能帮助我完成上面的动作,根本不用我抢啊,对的,这个机器人就是AccessibilityService,我们同样把抢红包流程化。 获取通知栏通知事件 点击通知栏消息 找到红包消息 点击 点击拆红包 返回 这每个步骤,也都不难啊,我们的工具类中,所有的方法都实现了,唯一要做的,就是写几个ifelse把逻辑拼起来就行了,具体代码就不贴了,毕竟是微信严打的一件事,大家适可而止就好了。 当然,这个Demo同样可以做的更完善一点,例如,增加WakeLock和Keyguard,实现在锁屏情况下的自动抢红包等功能。 微信自动回复 在了解了微信抢红包的方式之后,再看看微信自动回复,是不是就更是小菜一碟了?我们只要把抢红包的流程稍微改一下,就完成了整个功能的实现,不相信? 获取通知栏通知事件 点击通知栏消息 找到红包消息 ——> 输入自动回复的消息 点击 ——> 点击发送 点击拆红包 ——> 不需要了 返回 是不是非常简单?唯一一个有价值的代码如下: private void notifyWechat(AccessibilityEvent event) { if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); String content = notification.tickerText.toString(); String[] msg = content.split(":"); name = msg[0].trim(); text = msg[1].trim(); PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); } } } 一个简单的Trick而已,借用notification.contentIntent来唤起Notification对应的App。 实际上,我们能做的事情还有很多,当我们拿到对应的聊天信息时,可以通过聊天对象的筛选,来实现对『特别对象的监控』,例如你离开的时候,可以设置给你的老婆自动回复『亲爱的我在忙呢,等等哈』,而对其它人自动回复『滚,LZ忙』。再例如,可以对聊天信息进行分词、识别,从而实现对内容的精准回复,当然,这里还需要使用到一些第三方的语言分析软解,这里就不详解了,总之,没有想不到。 检查微信好友 那么再比如去年比较火的一个方法,通过拉好友进群组来检查是否还有好友关系。PC、Chrome上已经有很多软件来做这个检查了,其核心原理,都是通过拉群组的方式来做。那么在手机上,同样可以通过这种方式来实现,如果现在你还不知道该怎么做,那么后面的文章就没有看的必要了…… 进程清理 大家应该都用过冯老师的『绿色守护』,这个App的最基本无Root功能,就是通过在应用管理界面『结束进程』的方式来停止一个后台运行的App,大家都知道天朝的App,基本都是全家桶,所以这种方式对释放系统资源确实还是有一定的帮助的,那么我们就来看看简单的实现。 核心原理非常简单,在应用详情页面,通过停止服务来禁止App服务。OK,那么我们要做的,实际上,就是下面的流程: 通过Intent打开对应App的管理详情信息页面 点击停止运行 返回,处理下一个 流程要比抢红包什么的简单多了,下面列出2个关键代码,大家应用详情界面: public void cleanProcess(View view) { for (String mPackage : mPackages) { Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", mPackage, null); intent.setData(uri); startActivity(intent); } } 监控详情页面,进行停止操作: @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.getPackageName().equals("com.android.settings")) { CharSequence className = event.getClassName(); if (className.equals("com.android.settings.applications.InstalledAppDetailsTop")) { AccessibilityNodeInfo info = findViewByText("强行停止"); if (info.isEnabled()) { performViewClick(info); } else { performBackClick(); } } if (className.equals("android.app.AlertDialog")) { clickTextViewByText("确定"); performBackClick(); } } } 这个App唯一的难点,应该就剩下怎么把UI做的好看一点了。 另外,还有一个兼容性的问题,大家都懂的,国内各种第三方的ROM厂家,经常会修改一些系统的Activity,甚至不同系统版本同一个功能的Activity都有可能不一样,所以,使用AccessibilityService的一个比较大的麻烦就是兼容性的处理,需要使用dumpsys和uiautomator这些工具来进行详细的分析,这些工具的使用以及分析方法,在我的新书《Android群英传:神兵利器》中都有详细的讲解,想深入了解的开发者可以参考下。 判断应用当前状态 借助AccessibilityService同样可以做一些比较有用的事情,例如监控App当前的状态,例如前台、后台的切换,通过TYPE_WINDOW_STATE_CHANGED即可进行判断,特别是在5.0以上,原先的getRunningTasks这个方法被升级到系统权限。 当然,AccessibilityService或多或少会存在一些性能问题,所以现在并不推荐使用这种方式来监控应用状态,更多的是通过activitylifecyclecallbacks来实现对App状态的跟踪与监控。 出轨 其实一旦我们了解了AccessibilityService的使用原理,那么就很难做到不逾矩,毕竟这里的诱惑太大了,当我写到这里时,甚至有种不寒而栗的感觉,**所以这里申明: 本文所有内容仅供学习、技术交流,由此产生的各种问题,均与本人无关。** 防卸载 据我所知,已经有些App或者称之为恶意软件实现了这样的功能,这个功能难吗,不难,估计都不超过20行代码,但确实很恶心,特别是对一些普通、小白用户,压根都不知道AccessibilityService是什么,莫名其妙你让我启用,写的可能比较好看,什么帮助你清理系统,优化资源,但实际上,在后面做一些见不得人的事情。 我们来分析下如何实现,当用户想要卸载你的App的时候,一般会来到设置界面,找到你的App然后选择卸载,那么如果我们监控这个页面,如果发现是自己的App,就直接退出,这样不就无法卸载了吗?是的,代码如下,没几行代码: private String mDefenseName = "微信"; @Override public void onAccessibilityEvent(AccessibilityEvent event) { super.onAccessibilityEvent(event); if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.getPackageName().equals("com.android.settings")) { CharSequence className = event.getClassName(); if (className.equals("com.android.settings.SubSettings")) { AccessibilityNodeInfo nodeInfo = findViewByText("应用程序信息"); if (nodeInfo != null && findViewByText(mDefenseName) != null) { performBackClick(); } } } } 那么有人要说了,如果是用的一些第三方ROM,直接在桌面就能卸载呢?同样的,只不过会稍微麻烦点,需要判断的东西更多了,要处理的兼容性更复杂了而已。 这里不得不说,虽然国内各种第三方ROM百花齐放、肆意妄为,但这也给AccessibilityService造成了很大的兼容性处理难题,所以对一些恶意的使用AccessibilityService的App也形成了很大的限制。 浏览器劫持 实际上并不局限于浏览器,各种App都能被劫持,因为AccessibilityService监控的是全局App,良心点的可能会指定包名进行监控。所以,我们可以监控任意一个App,例如浏览器,一旦打开,我们就输入指定的网址,或者是一打开一些App,就输入一些查询内容,这里我以鄙司的沪江网校为例,进入后直接进行搜索。 算了代码还是不贴了,完全都是Copy前面的内容。 监控密码框 呵呵呵,这个你还真是想多了,系统再天真也不会把这个权限开放给你,所有的设置为password类型的EditText都是无法被监控的,系统还算有点良心。 这里我只列举了一些非常简单的Hack方式,但实际上,还有很多,例如通过拉取指定网站的内容后自动安装App并模拟点击等,当然,AccessibilityService也可以用在自动化测试中,这完全就是一把双刃剑,是利是弊,完全取决于使用他的人。 跳过用户授权 一般来说,AccessibilityService是需要用户手动操作授权才可以执行的,但是,如果是在Root的情况下,或者是在ADB连接PC的情况下,甚至都不用用户授权,就可以完成AccessibilityService的授权操作。 Root的情况就不说了,通过修改Setting的数据库就可以更改这个设置了,当然,有Root的情况下,就根本不需要AccessibilityService了。 在没有Root的情况下,如果PC通过ADB发出指令,同样是可以自动完成授权的,这个可以参考360的一篇文章: http://www.freebuf.com/articles/terminal/114045.html 我这里就不多说了,大家看看就懂了,并没有太多的技术含量,应该算是系统的一个小的漏洞。 AccessibilityService一般分析步骤 前面我们分析了那么多AccessibilityService好的不好的使用方法,实际上,总结下就这么几步。 分析操作的流程,拆解成单步可实现的过程 通过UIAutomator和adb shell dumpsys来查看对应的UI控件ID、文本或者是具体的Activity 通过逻辑组合进行代码编写 调试、兼容性处理 通过上面的这些方式,基本就可以实现一些固定流程的操作自动化了。关于AccessibilityService的工具类,我放到了Github上,虽然功能已经比较全了,但还没有经过很多的兼容性测试,同时,碍于时间和精力的关系,给出的Demo示例也不多,希望大家可以多提PR,共同完善。 https://github.com/xuyisheng/AccessibilityUtil 欢迎大家关注我的微信公众号:
前端日志与后端日志不同,具有很强的自定义特性,不像后端的接口日志、服务器日志格式比较固定,大部分成熟的后端框架都有非常完善的日志系统,借助一些分析框架,就可以实现日志的监控与分析,这也是运维工作的一部分。 什么是ELK ELK在服务器运维界应该是运用的非常成熟了,很多成熟的大型项目都使用ELK来作为前端日志监控、分析的工具。 那么首先,我们来了解下什么是ELK,ELK实际上是三个工具的集合: E:Elasticsearch L:Logstash K:Kibana 这三个工具各司其职,最终形成一整套的监控架构。 Elasticsearch ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。 我们使用Elasticsearch来完成日志的检索、分析工作。 Logstash Logstash是一个用于管理日志和事件的工具,你可以用它去收集日志、转换日志、解析日志并将他们作为数据提供给其它模块调用,例如搜索、存储等。 我们使用Logstash来完成日志的解析、存储工作。 Kibana Kibana是一个优秀的前端日志展示框架,它可以非常详细的将日志转化为各种图表,为用户提供强大的数据可视化支持。 我们使用Kibana来进行日志数据的展示工作。 以上三个框架,就构成了我们这套架构的核心。如果你想进一步了解这套架构,可以去他的官网上进行了解: https://www.elastic.co/ 这里也讲一个真实的故事——Elasticsearch项目的来历。 Elasticsearch 来源于作者 Shay Banon 的第一个开源项目 Compass 库,而这个 Java 库最初的目的只是为了给 Shay 当时正在学厨师的妻子做一个菜谱的搜索引擎。2010 年,Elasticsearch 正式发布。至今已经成为 GitHub 上最流行的 Java 项目,不过 Shay 承诺给妻子的菜谱搜索依然没有面世。 不得不说,还真是面向对象编程…… ELK架构图解 下面这张图很好的解释了什么是ELK: 当然,这也是最简单的ELK架构,在后端运维架构中,可能远不止如此,比如,还需要加入Kafka活这Redis等等,这里我们不做过多的讨论,我们只讨论最基础的架构。 ELK环境搭建 有同事问我,配置一套ELK环境需要多长时间,我说,大概需要20分钟,另外,其中大概有15分钟是在下载! 由于现在整个ELK项目基本上都已经被elastic这个公司收购了,所以,在它的官方网站上可以很容易的找到配置Guide。 https://www.elastic.co/start 按照这个配置指南,基本上很快就可以完成ELK的搭建,我们唯一需要做的,就是找到一份Log,然后配置下,让他展示出来就完了。 下载好tar包后,请尽量使用tar指令解压,不然就会像我的同事TT那样因为解压后的权限折腾上很长时间。 配置Logstash 我们首先需要在Logstash的文件根目录下创建一个配置文件,我这里举一个例子: input { file { path => "/Users/xuyisheng/Downloads/temp/log.txt" ignore_older => 0 sincedb_path => "/dev/null" } } output { elasticsearch{} stdout{} } 这个配置相信不用我多说,大家也能看懂,当然,这是一个非常基本的配置,只是从固定的文件中去读取Log信息并写入到elasticsearch,并不做任何处理工作。 写好配置文件后,只需要通过如下所示的指令启动Logstash即可: logstash-5.0.1 bin/logstash -f logstash.conf 启动之后,Logstash就会从文件中读取信息了。 配置Elasticsearch和Kibana 为什么Logstash我要单独讲,而Elasticsearch和Kibana我可以放一起讲呢?因为——这两个的配置实在是太简单了,简单到你根本不用配置任何东西…… 只需要两个指令就完成了,启动Elasticsearch: elasticsearch-5.0.0 bin/elasticsearch 启动Kibana: kibana-5.0.0-darwin-x86_64 bin/kibana OK,等程序启动完成,只需要打开localhost:5601就可以看见Kibana的界面了。 给大家看几张截图,简单的体会下它的强大就好(由于我这里项目是公司的,所以就从网上找了一些,是一样的) 这个是Kibana3的界面。 这个是Kibana5的界面,大家可以根据自己的需要选择不同的Kibana版本,反正配置都是一句话。 ELK的优势 ELK在运维上的优势我们就不具体的说了,什么分布式啊、什么消息队列、消息缓存啊,太多了,但我们其实并不用太关心。 强大的搜索 这是elasticsearch的最强大的功能,他可以以分布式搜索的方式快速检索,而且支持DSL的语法来进行搜索,简单的说,就是通过类似配置的语言,快速筛选数据。 强大的展示 这是Kibana的最强大的功能,他可以展示非常详细的图表信息,而且可以定制展示内容,将数据可视化发挥的淋漓尽致。 所以,借助ELK的这两大优势,我们可以让前端日志的分析与监控展现出强大的优势。 ELK使用场景 据我所知,现在已经有非常多的公司在使用这套架构了,例如Sina、饿了么、携程,这些公司都是这方面的先驱。同时,这套东西虽然是后端的,但是『他山之石,可以攻玉』,我们将这套架构借用到前端,可以使用前端日志的分析工作,同样是非常方便的。这里我举一些常用的使用场景。 业务数据分析 通过客户端的数据采集系统,可以将一些业务流程的关键步骤、信息采集到后端,进行业务流程的分析。 错误日志分析 类似Bugly,将错误日志上报后,可以在后端进行错误汇总、分类展示,进行错误日志的分析。 数据预警 利用ELK,可以很方便的对监控字段建立起预警机制,在错误大规模爆发前进行预警。 ELK的基本介绍就到这里,其实还有很多东西没有讲,例如使用Logstash对日志内容的处理、已经elasticsearch的搜索语法等等,如果大家有兴趣,可以在下面留言,如果感兴趣的人比较多,我会在后面的文章中进行进一步的分析。 一年一度的CSDN博客之星评选又开始了,欢迎大家给我投票: http://blog.csdn.net/vote/candidate.html?username=x359981514 有了各位的支持,我才有动力能够继续写出更多更好的文章,非常感谢大家的支持。
背景 事情的来由还要从几十几亿年前的一次星球大爆炸说起,sorry,背错台词了,是从几天前讨论接口返回数据和几个月前讨论课件本地数据结构说起,简单的说,就是碰到约定好的内容出现异常,是我们在程序中内部作兼容处理,还是抛出去。 打个比方,我们要解析一段json,约定这个json的格式,只能是正常格式,或者是空,那么一旦返回json的方法返回了一个『既不是正常格式,又不是空的异常值』,程序该如何处理呢? 小花:一旦碰到约定异常,程序必须兼容处理,一定不能让程序Crash 小Fa:一旦碰到约定异常,就必须抛出去,告知约定有误,找出具体错误原因 这个问题,相信只要是程序猿基本都遇到过,举个最常见的栗子,NullPointerException,假如我们要从json中取一个字段,突然发现发生了NullPointerException,一些开发者认为是数据问题,那么把json中的这个字段改正确就行了;还有一些开发者认为是程序问题,认为程序需要做非空判断,再去使用。我相信这两种程序猿都有自己的理由,第一种程序简洁明了,代码逻辑干净,但一旦出错,就会崩溃,第二种程序耐操,随你数据怎么错,我都能不Crash,但代码中到处存在非空判断,臃肿、重复。 生存还是毁灭,这是一个问题! 防御式编程 就在我们为了这个问题而争论的时候,突然有一个姓康的同事,施法祭出了一块砖头(《代码大全2》,近900页,相当于3本《Android群英传》),我一度以为他想砸在我的脸上,正当我准备闪避的时候,他翻到了这块砖头的第八章,几个大字赫然印入了我的视线——『防御式编程』。 果然是老司机,居然可以从防御性驾驶中悟出防御性编程,说好的编程不开车,开车不编程呢? 这位作者编程厉不厉害我不知道,但我知道,论开车,一定没有何老师diao! OK,《代码大全》给我们提供了一个定义——『防御式编程』,说白了,就是『人类都是不安全、不值得信任的,所有的人,都会犯错误,而你写的代码,应该考虑到所有可能发生的错误,让你的程序不会因为他人的错误而发生错误』 在书中,作者告诉我们,程序需要对可能的错误输入,做出兼容,例如一个除法的函数,你必须判断分母可能为0的情况,从而给调用者返回错误提示。另外,一般的高级编程语言,都提供了『断言』和『异常』两种方式来进行错误处理。 断言 断言,是一种在开发阶段使用的,让程序在运行时进行自检的代码,断言为真,那么程序运行正常,断言为假,那么程序运行异常退出。等等,防御式编程不是说好的要兼容异常吗,为什么会退出?实际上,作者的意思是,先断言、后处理错误,而断言是在开发环境中的,正式上线后是不会有断言的。 但实际上,这是一个悖论,开发阶段的错误处理代码在开发阶段被断言给拦截掉了,但错误处理代码也是人写的,那么如何去检测『错误处理代码可能发生的错误』呢? 异常 当代码出现问题时,可以通过抛出异常来进行通知,如果你无法处理,则可以交给外界进行处理。这个不多说,毕竟大部分代码,如果有异常,最简单的就是try catch了,我甚至见过把所以代码直接try catch的,你是有多不相信人类。 所以我觉得防御式编程用久了,会不会开始怀疑人生,果然,在往后翻几页,作者也给出了建议。 借用奇异博士的一句台词——『你TM居然把警告写在咒语的下一页』! 简而言之,防御式编程,就是持怀疑态度审视所有的代码,但这个和我们讨论的主题还是略有不同的,我们讨论的主题是『已经有了约定,但返回了约定之外的内容』。 契约式编程 就在我们讨论的时候,天空突然飘来五个字——那都不是事,哦不对,是『契约式编程』。 这个好像有点像!我们先来简单的看下什么是契约式编程,简单的说,契约作用于两方,每一方都会完成一些任务,从而促成契约的达成,但同时,每一方也会接受一些义务,作为制定契约的前提,有任意一方无视了必尽义的义务,则契约失败。 契约式编程要求我们在『前提条件』、『后继条件』和『不变量条件』进行契约的检查。类似的,例如检查参数,一旦参数不对,当即撕毁契约。这一点,现在很多新的语言都支持了,例如Swift,就支持对参数进行约束检查,这就是一种类契约式编程。 契约所约束的,是『一个为了确保程序正常运行的条件』,一旦契约被损毁,只有一个原因,那就是程序出了Bug,例如一个数据字段,在我处理的时候,必须保证是不为空的,那么谁来保证这一点呢,一定是我的调用方(或者说是其它模块),所以,一旦出现问题,应该有调用方来检查,确保调用的时候,必须是不为空的。 这让我想到了刚开始在面向日本人编程时期的一些事,日本人的做事风格是出了名的谨慎和详细,每一个方法、函数,在详细设计的时候,就已经把参数、返回值,已经它们的类型和所有可能的值都设计好了,每个方法之间有着明确的界限,如果你的方法因为传入的参数不在设计范围内而导致错误,你完全可以去找调用方,要求他按照设计来进行调用。不得不说,这应该是契约编程的最佳实践。日企普遍使用这种方式其实还有一个原因,那就是可以严格区分责任,让每个人都不必为了迁就他人的错误而进行『艰难的编码』。每个人按照契约处理好自己的事情,让损毁契约的人承担责任。 再引申一下,这和现在的『面向接口编程』也非常类似,两个模块之间,定义好调用、处理的接口,而具体的实现,对方都不用关心,只要安装协议的接口来进行开发就可以了,但光有接口也不够,还需要契约来做进一步的约束,例如参数、返回值的约束。 无独有偶,在《代码大全》中,作者也提出了『进攻式编程』,其实和契约编程,有异曲同工之妙。 乌托邦 OK,梦醒了,让阳光照进现实。以上两种编程方式,都是非常理想化的编程,但在一般的公司里面不论是防御还是契约,实现起来都是比较困难的,例如前端与后端的接口、不同部门同事的交流,按照契约式编程,没人Care你的契约,按照防御式编程,代码惨不忍睹,还容易漏掉防御。那么到底该怎么办呢,我认为,如果能在公司层面推广契约式编程,首先是对开发效率的提升,让每个人都对自己写的代码负责,在开发者之间建立良好的信任关系,同时也能减少不必要的沟通成本和精力。但同时,必要的防御式编程也是不能少的,这是保证程序健壮、稳定的前提。怎么说呢,中国人民秉承了千百年的传统——『中庸之道』,契约还是防御,视情况而定,这是平衡的艺术。 警告 本文请使用防御式阅读,每个人都会犯错,欢迎留言交流。 此文一出,很有可能引发双方混战,红与黑,天灾还是近卫,联盟还是部落,Choose your side。 欢迎关注我的公众号:
Clipboard是Android提供的一个系统服务,它提供了一个全局的剪贴板,让文字、图片、数据,在多App间共享成为可能,今天,我们来了解下它的真面目,以及被玩坏的新姿势。 老规矩,Google API文档镇楼: https://developer.android.com/guide/topics/text/copy-paste.html 说实话,如果不是为了让Clipboard玩出花,我真不想写这一篇,因为——这文档写的真是太TM详细了。 Clipboard应用 我们先来看看一些App对Clipboard的应用,例如手机迅雷,如果你复制了一个链接,那么打开迅雷后,会自动检测并提示下载: 再例如一些翻译软件,例如有道词典、沪江小D,他们都有一个功能,即复制查词,使用的也是这个原理,我这没装这些App,就不截图了,再例如比较常用的手淘喵口令,实际上也是利用这个功能,当然,也有一些比较专业的Clipboard App,例如Clipboard Actions: 我们可以看见,实际上,他就是帮你解析了各种可能的剪贴板,并对他们提供了各种后续功能的集合,确实非常实用,不过,看完今天的文章,相信你要写一个这样的App,估计也就分分钟。 OK,这些就是一些Clipboard的基本使用场景,更多场景,没有做不到,只有想不到。 基本使用 Clipboard的基本使用,就是三部曲。 获得ClipboardManager: ClipboardManager mClipboardManager = mClipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); Copy: ClipData mClipData; String text = "hello world"; mClipData = ClipData.newPlainText("test", text); mClipboardManager.setPrimaryClip(mClipData); Paste: ClipData clipData = mClipboardManager.getPrimaryClip(); ClipData.Item item = clipData.getItemAt(0); String text = item.getText().toString(); 结束了,简直不能再简单,API文档也写的非常详细,Demo都写了好几个。 不止于文字 我们可以创建以下三种类型的ClipData: 类型 描述 Text newPlainText(label, text) 返回ClipData对象,其中ClipData.Item对象包含一个String URI newUri(resolver, label, URI) 返回ClipData对象,其中ClipData.Item对象包含一个URI Intent newIntent(label, intent) 返回ClipData对象,其中ClipData.Item对象包含一个Intent 对应的,我们也能获取到不同类型的ClipData。 ClipboardManager管理 ClipboardManager中有很多判断与操作方法: 类型 描述 getPrimaryClip() 返回剪贴板上的当前Copy内容 getPrimaryClipDescription() 返回剪贴板上的当前Copy的说明 hasPrimaryClip() 如果当前剪贴板上存在Copy返回True setPrimaryClip(ClipData clip) 设置剪贴板上的当前Copy setText(CharSequence text) 设置文本到当前Copy getText() 获取剪贴板复制的文本 玩出一朵小FaFa 在了解了上面这些内容后,我们就可以做一些比较有意思的东西了,例如,我们可以通过监控用户剪贴板中的内容,来做一些自动的推断,例如,用户复制了一个英文单词,那么我们可以推断,用户可能要进行翻译,再例如,用户复制了一个链接,那么我们也可以推断,用户可能需要打开这个链接,等等。 Google在文档中,直接给出了示例的代码: // Examines the item on the clipboard. If getText() does not return null, the clip item contains the // text. Assumes that this application can only handle one item at a time. ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); // Gets the clipboard as text. pasteData = item.getText(); // If the string contains data, then the paste operation is done if (pasteData != null) { return; // The clipboard does not contain text. If it contains a URI, attempts to get data from it } else { Uri pasteUri = item.getUri(); // If the URI contains something, try to get text from it if (pasteUri != null) { // calls a routine to resolve the URI and get data from it. This routine is not // presented here. pasteData = resolveUri(Uri); return; } else { // Something is wrong. The MIME type was plain text, but the clipboard does not contain either // text or a Uri. Report an error. Log.e("Clipboard contains an invalid data type"); return; } } 其实非常简单,就是判断三种复制类型,但是我们可以在App中设置一些类似Scheme的标记,用来进行一些功能的区分,就好像淘宝的喵口令——『喵口令XXXXXXX喵口令』,我们可以通过解析这些Scheme,来获取内容,并进行对应的操作。这也是我们前面提到的Clipboard Actions这个App做的事情。 玩出一朵大FaFa 我们首先来看ClipData.Item.coerceToText()这样一个方法,这个方法可以将剪贴板里面的内容,直接转化为文字,但是这个转换,是有一定算法的,在API文档中有比较详细的说明,这里简单的看下: 这个东西能干什么呢,我们知道,有些App会复制之后,打开一个Intent,为了简单,会直接通过ClipData.Item.coerceToText()来返回一个Intent的URI,然后通过解析URI来启动Intent,那么这里就可以被我们来利用了。 public void fakeClipboard() { // 添加一个假的Intent,模拟用户最新加入的剪贴板内容 Intent intent = new Intent(); intent.setComponent(new ComponentName("com.hjwordgames", "com.hjwordgames.Splash")); intent.setAction("android.intent.action.VIEW"); ClipData setClipData; setClipData = ClipData.newIntent("intent", intent); mClipboardManager.setPrimaryClip(setClipData); // 呵呵哒 App以为获取的是自己需要的Intent,结果却被狸猫换太子 ClipData clipData = mClipboardManager.getPrimaryClip(); ClipData.Item myItem; myItem = clipData.getItemAt(0); String clipDataString = myItem.coerceToText(this.getApplicationContext()).toString(); try { Intent myIntent = Intent.parseUri(clipDataString, 0); startActivity(myIntent); } catch (URISyntaxException e) { e.printStackTrace(); } } 其实不一定是通过Fake Intent,其它的文字、图片等等,都可以被『偷天换日』。 另外,要实现这个监听,我们需要注册一个回调——addPrimaryClipChangedListener,Android真是体贴到没朋友: mClipboardManager.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() { @Override public void onPrimaryClipChanged() { Log.d("xys", "onPrimaryClipChanged: "); } }); 那么在这里,我们就可以完全实现剪贴板的『狸猫换太子』。那么假如我们是一个『某淘』软件的竞品,那么完全可以让『汪口令』失效,甚至替换为我们自己的应用,同理,还有一些翻译类软件也是一样,不过还好,也许是我的内心比较阴暗,目前还没有看见这样的App。 欢迎大家关注我的公众号:
一触即发 App启动优化最佳实践 文中的很多图都是Google性能优化指南第六季中的一些截图 Google给出的优化指南来镇楼 https://developer.android.com/topic/performance/launch-time.html 闪屏定义 Android官方的性能优化典范,从第六季开始,发起了一系列针对App启动的优化实践,地址如下: https://www.youtube.com/watch?v=Vw1G1s73DsY&index=74&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE 可想而知,App的启动性能是非常重要的。同时,Google针对App闪屏,也给出了非常详细的设计定义,如下所示。 https://material.google.com/patterns/launch-screens.html 其实最早的时候,闪屏是用来在App未完全启动的时候,让用户不至于困惑App是否启动而加入的一个设计。而现在的很多App,基本上都把闪屏当做一个广告、宣传的页面了,貌似已经失去了原本的意义,但闪屏,不管怎么说,在一个App启动的时候,都是非常重要的,设计的事情,交给UE吧,开发要做的,就是让App的启动体验,做到最好。 App启动流程 App启动的整个过程,可以分解成下面几个过程: 用户在Launcher上点击App Icon 系统为App创建进程,显示启动窗口 App在进程中创建自己的组件 这个过程可以用下面这幅图来描述: 而我们能够优化的,也就是下面Application的创建部分,系统的进程分配以及一些窗口切换的动画效果等,都是跟ROM相关的,我们无法处理。所以,我们需要把重点放到Application的创建过程。 上面是官方的说明,下面我们用更加通俗的语言来解释一遍。 当用户点击桌面icon的时候,系统准备好了,给App分配进程空间,就好像去酒店开房,但是你又不能直接进入房间,你得坐电梯去房间,那么你坐电梯的这个时间,实际上就是系统的准备时间,那么系统的这个准备时间一般来说不会太长,但假如的开的是一个总统套房呢,系统就得花不少时间来打理,所以系统给所有用户都准备了一个过渡界面,这个界面,就是启动时的黑屏\白屏,也就是你坐电梯里面看的小广告,看完小广告,你就到房间了,然后你想干嘛都可以了,这个想干嘛的速度,就完全取决于你开门的速度了,你门开得快,自然那啥快,所以这里是开发者可以优化的地方,有些开发者掏个钥匙要好几秒,有的只要几百毫秒,完全影响了后面那啥的效率。 那么一般来说,故事到这里就结束了,但是,系统,也就是这个酒店,并不是一个野鸡酒店,他也想尽量做得让顾客满意,这样才会有回头客啊,所以,酒店做了一个优化,可以让每个顾客自己定义在坐电梯的时候想看什么!也就是说,系统在加载App的时候,首先是加载了资源文件,这里就包括了要启动的Activity的Theme,而这个Theme呢,是可以自定义的,也就是顾客在坐电梯时想看的东西,而不是千篇一律的白屏或者黑屏,他可以定制很多东西,例如ActionBar、背景、StatBar等等。 启动时间的测量 关于Activity启动时间的定义 对于Activity来说,启动时,首先执行的是onCreate()、onStart()、onResume()这些生命周期函数,但即使这些生命周期方法回调结束了,应用也不算已经完全启动,还需要等View树全部构建完毕,一般认为,setContentView中的View全部显示结束了,算作是应用完全启动了。 Display Time 从API19之后,Android在系统Log中增加了Display的Log信息,通过过滤ActivityManager以及Display这两个关键字,可以找到系统中的这个Log: $ adb logcat | grep “ActivityManager” ActivityManager: Displayed com.example.launcher/.LauncherActivity: +999ms 抓到的Log如图所示: 那么这个时间,实际上是Activity启动,到Layout全部显示的过程,但是要注意,这里并不包括数据的加载,因为很多App在加载时会使用懒加载模式,即数据拉取后,再刷新默认的UI。 reportFullyDrawn 前面说了,系统日志中的Display Time只是布局的显示时间,并不包括一些数据的懒加载等消耗的时间,所以,系统给我们定义了一个类似的『自定义上报时间』——reportFullyDrawn。 同样是借用Google的一张图来说明: reportFullyDrawn是由我们自己调用的,一般在数据全部加载完毕后,手动调用,这样就会在Log中增加一条日志: $ adb logcat | grep “ActivityManager” ActivityManager: Displayed com.example.launcher/. LauncherActivity: +999ms ActivityManager: Fully drawn com.example.launcher/. LauncherActivity: +1s999ms 一般来说,使用的场景如下: public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public void onLoadFinished(Loader<Void> loader, Void data) { // 加载数据 // …… // 上报reportFullyDrawn reportFullyDrawn(); } @Override public Loader<Void> onCreateLoader(int id, Bundle args) { return null; } @Override public void onLoaderReset(Loader<Void> loader) { } } 但是要注意,这个方式需要API19+,所以,这里需要对SDK版本进行判断。 计算启动时间——ADB 通过ADB命令可以统计应用的启动时间,指令如下所示: ~ adb shell am start -W com.xys.preferencetest/.MainActivity Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xys.preferencetest/.MainActivity } Status: ok Activity: com.xys.preferencetest/.MainActivity ThisTime: 1047 TotalTime: 1047 WaitTime: 1059 Complete 该指令一共给出了三个时间: ThisTime:最后一个启动的Activity的启动耗时 TotalTime:自己的所有Activity的启动耗时 WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动) 这三个时间不是很好理解,我们可以把整个过程分解 1.上一个Activity的onPause()——2.系统调用AMS耗时——3.第一个Activity(也许是闪屏页)启动耗时——4.第一个Activity的onPause()耗时——5.第二个Activity启动耗时 那么,ThisTime表示5(最后一个Activity的启动耗时)。TotalTime表示3.4.5总共的耗时(如果启动时只有一个Activity,那么ThisTime与TotalTime应该是一样的)。WaitTime则表示所有的操作耗时,即1.2.3.4.5所有的耗时。 每次给出的时间可能并不一样,而且应用从首次安装启动到后面每次正常启动,时间都会不同,区别于系统是否要分配进程空间。 计算启动时间——Screen Record 通过录屏进行启动的分析,是一个很好的办法,在API21+,Android给我们提供了一个更加方便、准确的方式: ~ adb shell screenrecord --bugreport /sdcard/test.mp4 Android在screenrecord中新增了一个参数——bugreport,那么加了这个参数之后,录制出来的视频,在左上角就会增加一行数字的显示,如图所示。 在视频开始前,会显示设备信息和一些参数: 视频开始后,左上角会有一行数字: 例如图中的:15:31:22.261 f=171(0) 其中,前面的4个数字,就是时间戳,即15点31分22秒261,f=后面的数字是当前的帧数,注意,不是帧率,而是代表当前是第几帧,括号中的数字,代表的是『Dropped frames count』,即掉帧数。 有了这个东西,再结合视频就可以非常清楚的看见这些信息了。 启动时间的调试 模拟启动延时 在测试的时候,我们可以通过下面的方式来进行启动的延迟模拟: SystemClock.sleep(2000) 或者直接通过: try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } 或者通过: new Handler().postDelayed(new Runnable() { @Override public void run() { // Delay } }, 2000); 这些方案都可以进行启动延迟的模拟。 强制冷启动 在『开发者选项』中的Background Process Limit中设置为No Background Processes 优化点 Static Block 很多代码中的Static Block,都是做一些初始化工作,特别是ContentProvider中在Static Block中初始化一些UriMatcher,这些东西可以做成懒加载模式。 Application Application是程序的主入口,特别是很多第三方SDK都会需要在Application的onCreate里面做很多初始化操作,不得不说,各种第三方SDK,都特别喜欢这个『兵家必争之地』,再加上自己的一些库的初始化,会让整个Application不堪重负。 优化的方法,无非是通过以下几个方面: 延迟初始化 后台任务 界面预加载 阻塞 阻塞有很多种情况,例如磁盘IO阻塞(读写文件、SharedPerfences)、网络阻塞(现在应该不会了)以及高CPU占用的代码(加解密、渲染、解析等等)。 View层级 见《Android群英传》 耗时方法 通过使用TraceView && Systrace && Method Tracing工具来进行排查,见《Android群英传:神兵利器》 App启动优化的一般过程 通过TraceView、Systrace来分析耗时的方法与组件。 梳理启动加载的每一个库、组件。 将梳理出来的库,按功能和需求进行划分,设计该库的启动时机。 与交互沟通,设计启动画面,按前文方法进行优化。 解决方案 Theme 当系统加载一个Activity的时候,onCreate()是一个耗时过程,那么在这个过程中,系统为了让用户能有一个比较好的体验,实际上会先绘制一些初始界面,类似于PlaceHolder。 系统首先会读取当前Activity的Theme,然后根据Theme中的配置来绘制,当Activity加载完毕后,才会替换为真正的界面。所以,Google官方提供的解决方案,就是通过android:windowBackground属性,来进行加载前的配置,同时,这里不仅可以配置颜色,还能配置图片,例如,我们可以使用一个layer-list来作为android:windowBackground要显示的图: start_window.xml <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque"> <item android:drawable="@android:color/darker_gray"/> <item> <bitmap android:gravity="center" android:src="@mipmap/ic_launcher"/> </item> </layer-list> 可以看见,这里通过layer-list来实现图片的叠加,让开发者可以自由组合。 配置中的android:opacity=”opaque”参数是为了防止在启动的时候出现背景的闪烁。 接下来可以设置一个新的Style,这个Style就是Activity预加载的Style。 <resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> <style name="StartStyle" parent="AppTheme"> <item name="android:windowBackground">@drawable/start_window</item> </style> </resources> OK,下面在Mainifest中给Activity指定需要预加载的Style: <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.xys.startperformancedemo"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:theme="@style/StartStyle"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> 这里需要注意下,一定是Activity的Theme,而不是Application的Theme。 最后,我们在Activity加载真正的界面之前,将Theme设置回正常的Theme就好了: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { setTheme(R.style.AppTheme); super.onCreate(savedInstanceState); SystemClock.sleep(2000); setContentView(R.layout.activity_main); } } 在这个Activity中,我使用SystemClock.sleep(2000),模拟了一个Activity加载的耗时过程,在super.onCreate(savedInstanceState)调用前,将主题重新设置为原来的主题。 通过这种方式设置的效果如下: 启动的时候,会先展示一个画面,这个画面就是系统解析到的Style,等Activity加载完全完毕后,才会加载Activity的界面,而在Activity的界面中,我们将主题重新设置为正常的主题,从而达到一个友好的启动体验,这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。 异步初始化 这个很简单,就是让App在onCreate里面尽可能的少做事情,而利用手机的多核特性,尽可能的利用多线程,例如一些第三方框架的初始化,如果能放线程,就尽量的放入线程中,最简单的,你可以直接new Thread(),当然,你也可以通过公共的线程池来进行异步的初始化工作,这个是最能够压缩启动时间的方式 延迟初始化 延迟初始化并不是减少了启动时间,而是让耗时操作让位、让资源给UI绘制,将耗时的操作延迟到UI加载完毕后,所以,这里建议通过mDecoView.post方法,来进行延迟加载,代码如下: getWindow().getDecorView().post(new Runnable() { @Override public void run() { …… } }); 我们的ContentView就是通过mDecoView.addView加入到根布局的,所以,通过这种方式,可以让延迟加载的内容,在ContentView初始化完毕后,再进行执行,保证了UI绘制的流畅性。 IntentService IntentService是继承于Service并处理异步请求的一个类,在IntentService的内部,有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,而不需要去手动控制。 public class InitIntentService extends IntentService { private static final String ACTION = "com.xys.startperformancedemo.action"; public InitIntentService() { super("InitIntentService"); } public static void start(Context context) { Intent intent = new Intent(context, InitIntentService.class); intent.setAction(ACTION); context.startService(intent); } @Override protected void onHandleIntent(Intent intent) { SystemClock.sleep(2000); Log.d(TAG, "onHandleIntent: "); } } 我们将耗时任务丢到IntentService中去处理,系统会自动开启线程去处理,同时,在任务结束后,还能自己结束Service,多么的人性化!OK,只需要在Application或者Activity的onCreate中去启动这个IntentService即可: @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); InitIntentService.start(this); } 最后不要忘记在Mainifest注册Service。 使用ActivityLifecycleCallbacks Framework提供的这个方法可以监控到所有Activity的生命周期,在这里,我们就可以通过onActivityCreated这样一个回调,来将一些UI相关的初始化操作放到这里,同时,通过unregisterActivityLifecycleCallbacks来避免重复的初始化。同时,这里onActivityCreated回调的参数Bundle,可以用来区别是否是被系统所回收的Activity。 public class MainApplication extends Application { @Override public void onCreate() { super.onCreate(); // 初始化基本内容 // …… registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { unregisterActivityLifecycleCallbacks(this); // 初始化UI相关的内容 // …… } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } }); } } 资源优化 有几个方面,一个自然是优化布局、布局层级,一个是优化资源,尽可能的精简资源、避免垃圾资源,这些可以通过混淆和tinyPNG这些工具来实现。 甩锅方案 下面是两种不同的方案,都是在Style中进行配置: <item name="android:windowDisablePreview">true</item> 与 <item name="android:windowIsTranslucent">true</item> <item name="android:windowNoTitle">true</item> 我们先来看看这样做的效果: 设置效果类似,即通过取消、透明化系统的统一的加载页面来达到启动的『加速』,实际上,是一个『甩锅』的过程。强烈建议开发者不要通过这种方式去做『所谓的启动加速』,这种方式虽然看上去自己的App启动非常快,瞬间就完成了,但实际上,是将真正的启动界面给隐藏了。 系统说:这锅,我们不背! 无解 对应5.0以下的65535问题,目前只能通过Multidex来进行处理,而在5.0以下的机器上,系统在加载前的合并Dex的过程,有可能非常长,这也是暂时无解的问题,只能希望后面Multidex进行优化。 OK,App的启动优化基本如上,其重点过程,依然是分析耗时的操作,以及如何设计合理的启动顺序,希望各位能够通过文中介绍的方式来进行App的启动优化。 更多内容,请关注我的微信公众号:
模拟自然动画的精髓——TimeInterpolator与TypeEvaluator 在今天的文章开始之前,有个忙想请大家帮一下,希望在京东、淘宝、当当、亚马逊购买了我的书《Android群英传:神兵利器》的朋友们,帮忙去网店上给个简短的评价,举手之劳,还是多谢大家啦~~ 本文绘图软件 https://www.desmos.com/calculator 通过属性动画,我们可以模拟各种属性的动画效果,但对于这些属性来说,动画变化的速率和范围,是实现一个更加『真实、自然』的动画的基础,这两件事情,就是通过TimeInterpolator与TypeEvaluator来实现的。 TimeInterpolator与TypeEvaluator共同作用在ValueAnimator上,通过复合的方式产生最后的数据,这也就是数学上的『复合函数』,TimeInterpolator控制在何时取值,而TypeEvaluator控制在当前时间点需要取多少值。 由于这里涉及到两个变量,所以,这里我们通常使用『控制变量法』来进行这两个属性的研究,因为通常情况下,这两个属性的作用效果是殊途同归的。 TimeInterpolator 首先,我们研究TimeInterpolator,所以,将TypeEvaluator设置为默认,不产生任何修改。 TimeInterpolator,中文常常翻译成插值器。一个最简单的属性动画,示例如下: ObjectAnimator animator = ObjectAnimator.ofFloat(mTextView, "translationX", 0, mDistance); animator.setDuration(mDuration); animator.setInterpolator(new BounceInterpolator()); animator.start(); 通过setInterpolator方法,可以给Animator设置插值器,默认的插值器是AccelerateDecelerateInterpolator,即加速减速插值器。 理解TimeInterpolator的作用原理 TimeInterpolator是作用在时间参数上,例如我们有一个动画,时间从0到1,取值也从0到1,我们通过下面三条曲线来看同一时间点,取到的数值的不同。 当时间取0.5时,我们对应的y=x这条曲线,取出的是0.5,y=sqrt(x)这条曲线,取出的是0.25,y=x^2 这条曲线,取出的是0.7。也就是说,同一个真实的时间节点0.5,我们通过设置不同的函数曲线,取出了不同的数值,那么TimeInterpolator正是通过这种方式,来对时间参数进行修改,即,真实的时间0.5,对于其它两个函数,分别取出了模拟时间0.25和0.7所对应的值,从而达到了『篡改』时间的目的。 Android中的TimeInterpolator Android中已经给我们实现了很多TimeInterpolator,例如前面我们举的例子——AccelerateDecelerateInterpolator。我们打开AccelerateDecelerateInterpolator的源码。 其中关键的就是那行数学公式——(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f。我们来绘制下这个公式对应的曲线图(这里input的取值范围是0到1)。 在[0,1]区间内,就是我们的加速减速插值器了,结合字面意义很好理解。 那么在Android中,系统还给我们提供了非常多的TimeInterpolator,例如:AccelerateDecelerateInterpolator, AccelerateInterpolator, AnticipateInterpolator, AnticipateOvershootInterpolator, BounceInterpolator, CycleInterpolator, DecelerateInterpolator, LinearInterpolator, OvershootInterpolator, PathInterpolator。 大家可以通过API文档来找到这些插值器的定义,同时,通过源码来查看他们使用的数学公式。 自定义TimeInterpolator 自定义TimeInterpolator非常简单,我们参考系统自带的TimeInterpolator就可以实现了,即实现Interpolator接口和getInterpolation方法即可,例如: package com.xys.naturalanim.views.interpolator; import android.view.animation.Interpolator; public class CustomInterpolator implements Interpolator { @Override public float getInterpolation(float input) { return (float) Math.sin((input) * Math.PI * 0.5F); } } 其重点就是实现getInterpolation方法中的数学公式。 TypeEvaluator TypeEvaluator通常被翻译成估值器,在理解了TimeInterpolator之后,再理解TypeEvaluator就很简单了,一个系统自带的简单TypeEvaluator如下: 可见,它和TimeInterpolator基本一样,只不过实现的公式的参数不一样,但简单的换算一下,就通用了,例如: if (mInterpolator != null) { for (int i = 0; i < mViewWidth; i++) { mPath.lineTo(i, mViewHeight - mInterpolator.getInterpolation(i * 1.0F / mViewHeight) * mViewHeight); } } else { for (int i = 0; i < mViewWidth; i++) { mPath.lineTo(i, mViewHeight - (Integer) mTypeEvaluator.evaluate(i * 1.0F / mViewHeight, 0, mViewHeight)); } } 但是它们还是有一些细小的区别的,后面再细说,简单的概括,就是: TimeInterpolator控制动画的速度,而TypeEvaluator控制动画的值,他们可以共同作用,也可以单独作用(让另一个使用默认值)。 实际上,TypeEvaluator中的一个参数fraction,就是『复合函数』中TimeInterpolator计算的结果。即fraction=getInterpolation()。 自定义TypeEvaluator 这里首先讲一下TypeEvaluator的自定义,那么为什么要加呢,这是因为,这种方式限定了TypeEvaluator的类型是Number,那么这种就和TimeInterpolator几乎可以完全转化了,他们的目的都是通过提供的参数来完成曲线的绘制,从而实现对动画运动的控制。而TimeInterpolator只有一个参数,实现起来更加的简单,所以,大部分时候,我们都通过TimeInterpolator来实现这种运动曲线的模拟,所以,TypeEvaluator就这样没落了。 但是,不要以为TypeEvaluator就这样没用了,我们在小标题中也写了,是类型的TypeEvaluator可以进行转换,而TypeEvaluator实际上还有很多其它类型,在动画的坐标控制上,有奇效。 TypeEvaluator控制点的坐标 前面我们说了,Float类型的TypeEvaluator和TimeInterpolator基本是一样的,但TypeEvaluator并不只有Float这样一种,它有一种用的比较多的特性,就是通过TypeEvaluator来对运动坐标进行修改,将原本的直线坐标修改成曲线坐标,它通常会与ValueAnimator进行配合使用,例如下面的这个例子: 这种实现曲线运动的方式,就是通过TypeEvaluator来进行实现的,其中核心原理,就是通过Bezier曲线的De Casteljau算法计算出具体的点坐标,并设置给TypeEvaluator,代码如下所示。 public class BezierEvaluator implements TypeEvaluator<PointF> { private PointF mControlPoint; public BezierEvaluator(PointF controlPoint) { this.mControlPoint = controlPoint; } @Override public PointF evaluate(float t, PointF startValue, PointF endValue) { return BezierUtil.CalculateBezierPointForQuadratic(t, startValue, mControlPoint, endValue); } } Bezier的计算公式如下所示。 /** * B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1] * * @param t 曲线长度比例 * @param p0 起始点 * @param p1 控制点 * @param p2 终止点 * @return t对应的点 */ public static PointF CalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) { PointF point = new PointF(); float temp = 1 - t; point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x; point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y; return point; } 所以,综上所述,在作动画速率曲线控制的时候,使用TimeInterpolator即可,如果要改变点的坐标,就可以使用TypeEvaluator。 自然动画 在了解了TimeInterpolator和TypeEvaluator之后,我们就可以来了解下动画展现的优化方式了,普通的动画默认以线性的方式展现,但带来的后果就是动画效果的『僵硬』,动画本来是模拟两个状态的过渡过程的,这个在自然界中是『自然、流畅』的,所以,我们不能通过线性的数据变化来模拟自然动画,这就需要使用TimeInterpolator和TypeEvaluator来设计动画曲线了,通过它们来控制动画的实现过程,从而实现动画的展示,这就是我们来实现自然动画的的基本方式。 缓动函数 既然线性的动画曲线无法满足我们的动画模拟需求,那么就需要通过一定的数学公式来改变这些动画曲线,值得庆幸的是,这些事情有人帮我们做过了,有人专门设计了这样一些动画的曲线库。 http://easings.net/zh-cn 就是这样一些缓动函数库,让我们在设计动画的时候,可以作更加真实的模拟。同时,你也可以设计自己的曲线函数,下面这个网站,就可以实现这样的模拟。 http://inloop.github.io/interpolator/ 自然动画的模拟演示 在各位前辈的肩膀上,我这里撸了一个演示的Demo库,界面如图。 这里主要有几个功能: 可以选择不同的TimeInterpolator 可以选择要演示的动画效果,包括位移、缩放、旋转、透明度 演示包含两个View,上面的是设置对应动画模拟效果的View,下面的是对照的线性效果的View 一个动态图简单的了解下: 代码已经开源到Github: https://github.com/xuyisheng/NaturalAnim 欢迎大家提交自己的函数曲线-插值器。 欢迎大家关注我的微信公众号:
推送 推送简直就是一种轻量级的骚扰方式 自从有了推送,各个公司基本上都在使用推送,这确实是一个比较好的提醒方式,Android较iOS强的一个部分,也就是在于Android的Notification。Google教育我们利用好Android的通知模块,做更多友好的交互,可这句话,翻译成中文,不知不觉,就变成了在Notification中推送各种广告,而且仅仅就是一些广告,Notification各种牛逼的功能,完全不需要,这也违背了Google设计Notification的初衷。 更关键的是,现在随便找一款App,没有推送的真是凤毛麟角,更可恶的是,做外卖的App给我推送奥运新闻,一条新闻十几个App推送,以至于现在很多用户都非常反感各种推送广告,就我本人而言,基本上会禁用所有广告类的App的推送。 本人非常反感推送,借用王思聪的一句话,XXX App天天给我推送各种广告,还TM是自己做的推送,真是绝了。 推送方案 轮询 轮询是最简单的与服务器保持通信的方式,即循环向服务器通信。这个方案的特点就是通信由客户端主动发起,你需要自己实现轮询消息队列、频率等等参数,在功耗和效果间做权衡,类似于TCP的短连接。 SMS 这个其实就是借助短信来实现信息的展示,只不过把短信内容展示到了Notification中,这个方案,到达率确实高,毕竟短信是比较可靠、稳定的,但劣势也很明显,就是成本很高,而且在Android平台上,短信的权限比较开放,容易被劫持。 长连接 长连接和前面提到的短连接,都是基于Socket连接的方式,他们的区别在与,短连接是每次数据传输完毕后就断开连接,而长连接不会。所以,基于轮询的方式,每次都要进行链路的连接,性能消耗更大,基于长连接的方式,就是对这点的改进。应用一旦与服务器连接成功,并不会主动断开连接,后面的通信都基于这个通道。目前大部分的推送服务都是基于长连接的推送,在后台维护一个Service,维持应用与服务端之间的TCP长连接。 推送方案 iOS iOS这边使用系统统一的APNs,所有推送消息都由苹果的服务器进行下发,同时,也由系统进行统一展示和处理。 GCM 与iOS一样,Android同样有一套内置的推送方案,但很可惜的是,Google的服务在中国大陆无法使用,草了个蛋。 第三方推送服务 专业的第三方推送 极光 个推 友盟推送 手机ROM厂商推送 华为推送 小米推送 BAT级别的全家桶 阿里推送 信鸽推送 百度推送 关于第三方推送服务在各个App中的使用率,大家可以参考贾吉鑫的这篇文章: https://mp.weixin.qq.com/s?__biz=MzA5OTMxMjQzMw==&mid=2648112527&idx=1&sn=b23c1b5f3e32e343ad96d705bd4d63ff 第三方推送注意 这些推送服务大同小异,基本上一家使用了一个新功能,另外几家,也会很快推出这个功能,就例如之前比较火的,『共享推送通道进行App唤醒』这个技术,友盟、个推推出后,很快其它推送服务商就支持了,所以开发者并不需要担心哪一家推送功能比较强。 这里还需要说下现在的『推送唤醒』这样一个功能,简单的说,就是所有安装了A推送的App,只要有一个还活着,就可以把其它安装了A推送的App拉起来,从而提高推送的到达率。有些阿里系、百度系的App,被大家称作『全家桶』,实际上就是因为这个原因,这个方式,确实能在一定程度上提高推送到达率,但另一方面,也破坏了Android生态,增加了功耗,打乱了系统的清理策略。 另外,小米推送、华为推送,大家接入的原因可能很简单,就是他们的手机市场占有率比较高,接入他们自家的推送,可以在一定程度上提高到达率,但需要注意的是,推送分为透传和非透传两种方式,透传即我们自己App处理推送消息,而非透传,则是交给相应的PushSDK处理,对于小米推送、华为推送来说,只有采用非透传消息,到达率采用保证,而透传消息,与其它推送并没有什么区别,换句话说,小米手机、华为手机,只对非透传的推送消息做了可靠性保证,但非透传消息的展示格式非常固定、简单,且不能自定义,这是一个很大的问题,这点应该是很多开发者的误区。 最后,很多推送服务都需要在Application中进行初始化,同时,各种被唤醒策略,又会拉起Application,导致推送进程的重复,所以,这里经常需要对进程名进行过滤,非主进程,不进行初始化。 自建推送服务 基本都是基于AndroidPN、MQTT、XMPP、长连接这些方式去实现的,自己搭建Push平台服务,一个最大的问题就是服务端的架构设计,不仅成本高,而且效果不一定好,建议中小企业不要轻易尝试。 推送名词解释 RegistrationID\ClientID 一般来说,类似这类ID都是用于唯一标识应用\用户的,每个App在每台手机上都会生成一个唯一ID。 RegistrationID\ClientID生成规则 Android平台上因为国内存在大量山寨设备,所以很多设备的IMEI、Mac地址、AndroidID 都有可能为空或者错误,所以不能单独作为唯一标识,需要将这些进行组合起来使用。 对于应用卸载后RegistrationID的问题,很多PushSDK的策略是,生成一个DeviceID保存到本地存储,应用被卸载后如果被重新安装,如果检测到存储里的DeviceID还在的话,就判定是同一个设备,不重新生成RegistrationID。 AppKey\AppID 这些Key基本都是用于验证App的,每个包名对应一个加密的Key。 透传\非透传 非透传消息是指推送消息被PushSDK获取并处理,透传消息是指推送消息被PushSDK交给宿主应用处理,非透传消息通常只能设置一些固定的样式,比较简单,而透传消息,可以由App自定义处理,比较灵活。 推送数据基础 累计注册 通过应用使用的appid统计用户注册总量。 日在线用户 通过应用使用的appid统计当天的在线用户数。 活跃用户 通过应用使用的appid统计当天在推送平台激活过的用户总数。 在线下发率 在线消息下发数/总下发数。 回执率 消息回执数(去重)/消息在线下发数。 到达率 到达数/实际下发数。 百日内联网用户数(可推送用户数) 是指最近三个月内有登录过(设备与推送服务端建立长链接)的设备总数,即有效可下发的用户数。一般的推送服务端认为,设备在100天内没有登录请求,认为该设备已经失效,所以无需再次发送。 实际下发数 实际可推送设备数(在消息有效期内,有联网并推送进程正常的设备,即消息有效期内的在线下发数。消息有效期就是设置的离线时间)。 到达数 客户端SDK接收到消息的设备数(通过统计客户端SDK接收到消息后的回执获得)。 展示数 用自定义非透传消息在用户手机展示过的设备数。 点击数 点击通知栏消息的设备数。 推送数据分析 那么关于推送,大家实际上最关系的,就是『到达率』。那么这个到达率究竟怎么计算呢? 首先我们举个例子来说明上面的这些数据背后的实际意义,例如,我们有一款App,有100w的下载量,每个App启动后,都将上报给服务器一个唯一ID,所以,累计注册量就是100w,也称发送总量。 那么在服务端准备发送推送的时候,当前手机端推送进程还活着的,也就是说推送的长连接还健在的,就是在线设备,如果按天算,那么就叫日在线设备数,我们假设这个数字是60w。 OK,推送发出去后,客户端收到推送消息,并产生回执,代表完成了一次推送,假设这些完成推送的设备是55w,这个就是送达设备数。一般来说,只要设备在线,基本都能送达,所以这个数字和在线设备数非常接近,不接近的话,这个推送基本就有问题了,其中可能送不达的原因就在于网络切换等导致长连接断掉这类因素。 那么到这里,一般的推送服务商会使用送达设备数/在线设备数的方式来计算到达率,当然,前面我们也说了,这个比例一定是很高的,如果保持长连接的设备都不能收到推送,那一定是有问题了。 而一般的到达率,应该是送达设备数/可送达设备数,也就是百日内活跃的设备数,这样一除,这个比例一下子就小了很多,因为谁也不知道,这一百天内曾经活跃的用户,第二天是不是就已经把你卸载了。所以说,Android下统计推送的到达率一般都很低,而推送服务商宣传的到达率都很高,这其实就是一个偷换概念的问题,我们说的是一般的到达率,而服务商宣传的是在线到达率。 而且,这个到达率与iOS完全没有可比性,因为iOS统一通过APNs来进行推送,且无法获取到达回执,所以,iOS基本不存在到达率这一说法,如果有,几乎也是100%,完全没有意义,所以,如果哪一天有产品或者运营跟你说,为什么Android的到达率比iOS的到达率差这么多,请毫不客气的砸它一斤苹果。 Tag\Alias Tag Tag,或者叫标签,是用户的一种属性,在给某些用户设置某类标签后就可以针对推送。比如给喜欢『编程』的人打上『编程』的标签,就可以只给他们精准推送。 通常情况下,一个设备(在一个App里)可以设置多个标签。标签与别名类似,其对应关系也是保存在推送服务器侧的。 Alias Alias,或者叫别名,是对已经安装某应用的用户取个别名进行标识,在对该用户消息推送时,就可以用此别名来进行推送。设置了别名后,推送时服务器端指定别名即可。推送服务器端来把别名转化到设备ID来找到设备。 Tag和Alias他们的共同点在于,提供对用户的精确推送。 推送原理 目前大部分的第三方推送服务,都是基于长连接的推送方案,下面将对这个方式进行详细讲解。 NAT 首先,我们需要了解下一个网络基本知识——NAT,即网络地址转换(Network Address Translation,NAT),这是因为IP地址是有限的,手机无论是通过路由器还是数据网络,都有一个内网IP地址,同时,路由器上会维护一个外网IP地址,从而形成一个NAT路由表,即内网IP地址:端口,以及对应的外网IP地址:端口。这样通过一层层封装与解封装,就达到了内网与外网交换通信的方式。 NAT超时 由于NAT路由表的大小有效,所以一般路由都有NAT有效期,WIFI下,这个NAT有效期可能会比较长,而在数据流量下,运营商一般都会尽快更新NAT路由表,淘汰无效的设备,所以,在使用数据流量时,长连接经常容易断。 那么除了NAT路由表主动淘汰过期的设备之外,切换网络环境和DHCP服务器租期到期,这些情况都有可能导致NAT路由表改变,从而造成长连接中断。 心跳包 前面我们说了,现在的推送服务一般采用的是长连接的通信方式,而长连接会因为NAT路由表的更新而中断,所以,客户端会定时向服务端发送一个心跳包,来定期告知NAT路由表,我还活着,别杀我!这就是心跳包的作用——防止NAT路由表超时,同时检测连接是否被断开。 心跳包的心跳时间 既然心跳包的作用是防止NAT超时,那么就需要将心跳包的发送频率设置为小余NAT超时的检测频率,而WIFI和数据流量下,对于NAT路由表的超时时间又是不一样的,而且不同的网络运营商的超时时间,甚至都不一样,所以,一个比较好的方法就是根据设备当前网络环境,来动态的设置心跳时间。 注意,心跳包与轮询是不一样的,心跳包建立在长连接上,只要发送数据即可,而轮询每次都是一个完整的TCP连接。 心跳包谁来发 既然需要定时任务,那么就需要使用AlarmManager来作定时唤醒了,原因我之前的文章有讲过,是关于处理器唤醒的原因,这里就不赘述了,大家可以参考我之前的文章: http://mp.weixin.qq.com/s?__biz=MzAxNzMxNzk5OQ==&mid=2649484680&idx=1&sn=bd9086a95b769af8d8644cf681ce66ec#rd 进程保活 所谓进程保活,是指App希望尽可能的保证自己的App的推送进程能够存活在后台,以保证可以收到服务端的推送消息,因此,才出现了一大批关于进程保活的方式,例如NDK层的文件锁,fork子进程、前台服务、进程优先级等等方式,然而,这些东西,实际上,都不能完全保证手机的进程管理策略放过你,特别是Android 5.0以后的系统,Android对进程的管理更加严格,还有国内的这些ROM层的修改,ROM想要杀你这个进程,你怎么做也没有办法,哦,除了白名单。所以,不要再花心思去找什么进程保活的黑科技了,好好做好应用,提供用户的使用黏性,才是最佳的保活,而对于一些产品、运营所谓的『为什么微信、QQ都可以保活』这样的问题,我建议你回答它:『如果你能把产品做到微信、QQ那样的数量级,我也能让你活!』 推送整合方案 介于各种第三方推送与ROM推送的特点,我们目前采用的推送方案,名为『UniversalPushSDK』,即整合了多个不同的推送渠道,通过模板设计模式来进行整合,并向外暴露统一的接口,这种方式的好处在于UniversalPushSDK利用的各个不同推送特点,提高推送到达率,但是坏处在于,包的体积会大一些。例如,我们现在整合了『小米推送、极光推送、华为推送』,在系统启动的时候,判断当前系统,如果是小米系统,则启用『小米推送』,如果是华为手机,则启用『华为推送』,其它的Android设备,则启用『极光推送』,通过这种方式来设计我们自己的推送SDK,可以在一定程度上,利用好各个推送平台的特性。 那么如果利用这种方式来设计SDK给到不同的App接入,就需要能够将应用的推送Key做到动态配置,这也是我们遇到的最大的一个问题,解决方法大家可以参考我之前写的一篇文章: http://blog.csdn.net/eclipsexys/article/details/51283232 虽然我极力反对这种方案,我坚持认为,做好App,提升用户使用黏性,才是提升推送到达率的关键
我的新书《Android群英传:神兵利器》刚刚上市不久,得到了很多开发者的鼓励和肯定,我在此表示由衷的感谢! 本篇为本书的勘误,由于时间仓促,书中难免会存在一些错误,特在此列出这些勘误,也希望广大读者发现错误后,及时在本文评论中贴出来,我将收录到下次的修订中,感谢大家的支持和包容~~ 第二章Git 这一章中的代码都是从Mac终端中直接复制出来的,有些开发者可能不太熟悉终端的显示格式,所以看上去可能有点疑惑,比如这张图: 目录名和操作指令显示在了一起,有朋友跟我反映说,这里可能显示不是很清楚,所以,我在这里给大家提醒下,如果有疑惑的读者,可以参考下~~ 其中,红框是终端当前的目录名,蓝框是我们的操作指令,黄框是进入git目录后的提示内容。 附录A Android Studio快捷键 附录A的第一页最下面两行快捷键重复了! 望读者知悉,快捷键重复。 同时,附录中有些地方的『代码块』,写成了『代码快』,抱歉。 另外,关于有些快捷键没有收录的问题,因为我很久没有使用过Windows了,毕竟由俭入奢易,由奢入俭难啊(但是我书里讲了如何在其它平台下寻找快捷键)。 我当时写书的时候,也是忽略了这个问题,等我想起来的时候,已经来不及了,所以,这里也希望广大读者朋友留言,将Windows下的快捷键贴出来。 在我的公众号中,也给出了如何查找不同平台下快捷键的方法: http://mp.weixin.qq.com/s?__biz=MzAxNzMxNzk5OQ==&mid=2649484695&idx=1&sn=6a24acae75613021d11a524c9f864a3b#rd 大家可以看一下。 5.9开发者选项 这里手抖了,应该是Strict Mode!! 2.8远程仓库P76 P76页的『拉取本地代码后』,应该是『拉取远程代码』。 4.3更改项目结构P164 这里我遇到了Android Studio的一个大坑,也是我的一个失误,多谢读者的指出。 这是图4.8 自定义的项目结构 我通过sourceSets指定了新的资源Layout目录,而且编译通过了,以为没有问题了,但实际上,这里在运行时是无法找到资源的!必须在新建的目录下,创建一个默认的名为layout的目录才可以,如上图所示。 可见,在activity和fragment目录下,创建了一个layout目录,这样才可以正常被使用!!所以应当用这张图替换图4.8,同时,原文中的『在原有src/main/res资源目录的基础上,增加了两个新的目录,即src/main/res/layout/activity和src/main/res/layout/fragment』也应当改为『在原有src/main/res资源目录的基础上,增加了两个新的目录,即src/main/res/layout/activity/layout和src/main/res/layout/fragment/layout』 希望大家注意!非常感谢这位读者的指出! 5.7UIAutomatorViewer P257 这里的『在终端中输入iautomatorviewer』应当为『uiautomatorviewer』,少写了一个u。 2.3提交修改P61 原文中『并提升使用git add』应改为『并提示使用git add』。 3.2单词选择P98 原文中『提供了安装驼峰命名法』应改为『提供了按照驼峰命名法』。 4.3Gradle进阶P166 原文中『但坏外是如果这样写』应改为『但坏处是如果这样写』。 公众号 欢迎关注我的公众号,因为如果你买了书,这个公众号就是你的售后、保修、客服,关注公众号,你会有意想不到的好东西~
Android群英传:神兵利器 2016年的第一本书,比2015年来的稍早一些 《Android群英传:神兵利器》——看上去好像是第一本书的续集,但实际上,这本书的内容,在我写《Android群英传》的时候就已经写了不少了,碍于出版社的篇幅限制与主题的统一,很多内容并没有放到《Android群英传》中。 由于第一本书上市后,受到各位开发者的抬爱,销售情况还算理想,所以出版社一直希望我能出一本续集,因此,我便萌生了想要把这本书补全的想法。 一开始,我一直在思考,开发者到底需要怎样的书,作为一个一线的开发者,我的经历还是比较丰富的,头衔比较长——自学者、业余开发者、私活开发者、布道师、职业开发者。从一开始的自学,到利用业余时间开发,甚至接一些小的项目,到成为在线教育讲师、最终成为专职开发者,我经历了大部分Android开发者都未必经历的全的过程。这也成为我的一笔宝贵财富,让我可以体会各个不同阶段的开发者的痛点和瓶颈。 从程序员到工程师的进阶之路 硅谷一直流行着一种工程师文化,这种文化也是开发者引以为豪的文化,那么究竟什么是工程师文化呢,我认为,首先是要拥有创造的精神,拥有不断学习的能力,其次,需要有工具为王的意念,最后,需要执着、热爱自己的工作。 第一点,是我在《Android群英传》中所想要展现的,希望能够告诉开发者如何去学习、如何去建立自己的知识架构体系,从而能够激发自己创造的兴趣。 第二点,是我在《Android群英传:神兵利器》中所想要展现的,希望能够告诉不同阶段的开发者,如何通过使用工具来帮助自己更好的去驾驭这些知识。 第三点,我相信不是他人能够指点的,唯有自己去体会。 工具为王 我们都说,不要重复造轮子,善用工具,是提高效率和加速进化的关键,工具,才是王道,才是第一个拿起木棒的人猿得以进化的根源。《Android群英传:神兵利器》这本书,我没有再继续写Android开发进阶的一些技巧,因为我相信,看完《Android群英传》的读者,应该有能力能够自己去总结、学习自己的知识架构了,因此,我想要写一本完全不一样的书,一本完全讲解工具的书,我想利用自己的开发经历,告诉读者,如何通过工具来提高自己的开发效率。 这个世界上没有什么是不能用工具来解决的,如果有,那就创造一个工具去解决——尼古拉斯·医生 那么要讲解哪些工具呢?《Android群英传:神兵利器》一共总结了七大神兵利器: 开发环境搭得好,程序设计乐逍遥 项目要想跑得好,版本控制不可少 Android Studio 大揭秘,省出时间玩游戏 与Gradle 的爱恨情仇,让你一次爱个够 珍视身边的朋友,从开发者工具做起 探究性能秘史,了解尘封往事 个人团队轮流转,工具真情长相伴 相信大家猜也能猜到了,这本书介绍了从个人开发者到团队开发者,从入门开发者到资深开发者平时的开发中所会用到的绝大部分工具。 这本书讲什么 这个应该是大家最关心的话题了,我觉得学习,最重要的是学习的方法,这个我在第一本书《Android群英传》中,已经讲解过了。其次,是掌握正确的工具,这也是我想在《Android群英传:神兵利器》中想要讲解的,并且,书中还加入了我作为个人开发者时期以及在团队开发中的一些经验和技巧。 本书共分为 7 章,分别是: 第 1 章主要讲解如何搭建一个优雅、令人愉悦的开发环境。开发者绝不是“码农”, 而是要去享受创造的乐趣的,所以一个高效的开发环境就显得尤为重要了。正所谓——开 发环境搭得好,程序设计乐逍遥。 第 2 章讲解协同开发最重要的工具——Git。它可以说是目前团队开发的基础,也是版 本控制的核心工具。正所谓——项目要想跑得好,版本控制不可少。 第 3 章主要讲解 Android Studio 的一些不为人知的使用技巧,发掘出 Android Studio 作为一个强大工具的巨大力量。正所谓——Android Studio 大揭秘,省出时间玩游戏。 第 4 章主要讲解 Android 最新的编译工具 Gradle 的使用技巧。虽然 Gradle 的学习曲线 比较陡峭,但如果说 Android Studio 是一把宝剑,那么掌握好 Gradle,就好比一块磨刀石, 可以把宝剑打磨得愈发锋利。正所谓——与 Gradle 的爱恨情仇,让你一次爱个够。 第 5 章主要讲解 SDK 和开发者选项中提供的工具的使用方式。这些工具也是开发者 最容易忽视的工具。正所谓——珍视身边的朋友,从开发者工具做起。 第 6 章主要讲解 Android 提供的一些性能优化的工具及其使用技巧。利用好这些工具, 是进行性能优化的必备前提。正所谓——探究性能秘史,了解尘封往事。 第 7 章主要讲解个人开发者和团队开发者在学习、工作中经常使用的一些工具。正所 谓——个人团队轮流转,工具真情长相伴。 OK,下面就给出这本书的详细目录。 关注我 我的公众号:Android群英传 后续会在公众号中继续给大家带来一些精彩的文章~ 我的第一本书:Android群英传 这里也给出我的第一本书的购买链接,欢迎各位打包购买。 看清楚是第一本书啊 不是新书 新书的购买链接在后面!!! 京东 当当 亚马逊 抽奖 国际惯例,新书抽奖,过几天,我将在微博和本公众号进行抽奖,大家想怎么抽奖,可以各抒己见,给我留言即可。 微博:http://weibo.com/eclipsexu/ 公众号: 请早日关注哦 ~ 梦想还是要有的,说不定就中了呢? 购买链接 先放上高清无码大图 希望大家喜欢~ 购买链接: 这才是新书的购买链接!!! 京东 亚马逊 天猫 如果觉得本书对你的开发有帮助,请点击原文链接,在我的博客最下方,找到购买链接,或者直接在京东、当当、亚马逊等网上书店搜索『Android群英传 神兵利器』进行购买。 现在网店还处于预售阶段,可以正常购买,等网店到货后,会第一时间发货给预订的读者,预计在8月26日左右发货(亚马逊)。
PathMeasure之迷径追踪 Path,不论是在自定义View还是动画,都占有举足轻重的地位。绘制Path,可以通过Android提供的API,或者是贝塞尔曲线、数学函数、图形组合等等方式,而要获取Path上每一个构成点的坐标,一般需要知道Path的函数方法,例如求解贝塞尔曲线上的点的De Casteljau算法,但对于一般的Path来说,是很难通过简单的函数方法来进行计算的,那么,如何来定位任意一个给定Path的任意一个点的坐标呢? Android SDK提供了一个非常有用的API来帮助开发者实现这样一个Path路径点的坐标追踪,这个类就是PathMeasure,它可以认为是一个Path的坐标计算器。 初始化 PathMeasure类似一个计算器,对它进行初始化只需要new一个PathMeasure对象即可: PathMeasure pathMeasure = new PathMeasure(); 初始化PathMeasure后,可以通过PathMeasure.setPath()的方式来将Path和PathMeasure进行绑定,例如: pathMeasure.setPath(path, true); 当然,你也可以直接使用PathMeasure的有参构造方法来进行初始化: PathMeasure (Path path, boolean forceClosed) 这里最不容易理解的就是第二个boolean参数forceClosed。 forceClosed参数 这个参数——forceClosed,简单的说,就是Path最终是否需要闭合,如果为True的话,则不管关联的Path是否是闭合的,都会被闭合。 但是这个参数对Path和PathMeasure的影响是需要解释下的: forceClosed参数对绑定的Path不会产生任何影响,例如一个折线段的Path,本身是没有闭合的,forceClosed设置为True的时候,PathMeasure计算的Path是闭合的,但Path本身绘制出来是不会闭合的。 forceClosed参数对PathMeasure的测量结果有影响,还是例如前面说的一个折线段的Path,本身没有闭合,forceClosed设置为True,PathMeasure的计算就会包含最后一段闭合的路径,与原来的Path不同。 API PathMeasure的API非常容易理解,几乎都是望文生义。 getLength PathMeasure.getLength()的使用非常广泛,其作用就是获取计算的路径长度。 getSegment boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo) 这个API用于截取整个Path的片段,通过参数startD和stopD来控制截取的长度,并将截取的Path保存到dst中,最后一个参数startWithMoveTo表示起始点是否使用moveTo方法,通常为True,保证每次截取的Path片段都是正常的、完整的。 如果startWithMoveTo设置为false,通常是和dst一起使用,因为dst中保存的Path是被不断添加的,而不是每次被覆盖,设置为false,则新增的片段会从上一次Path终点开始计算,这样可以保存截取的Path片段数组连续起来。 nextContour nextContour()方法用的比较少,比较大部分情况下都只会有一个Path而不是多个,毕竟这样会增加Path的复杂度,但是如果真有一个Path,包含了多个Path,那么通过nextContour这个方法,就可以进行切换,同时,默认的API,例如getLength,获取的也是当前的这段Path所对应的长度,而不是所有的Path的长度,同时,nextContour获取Path的顺序,与Path的添加顺序是相同的。 getPosTan boolean getPosTan (float distance, float[] pos, float[] tan) 这个API用于获取路径上某点的坐标及其切线的坐标,这个API非常强大,但是比较难理解,后面会结合例子来讲解。 简单的说,就是通过指定distance(0 硬件加速的Bug 由于硬件加速的问题,PathMeasure中的getSegment在讲Path添加到dst数组中时会被导致一些错误,需要通过mDst.lineTo(0,0)来避免这样一个Bug。 Demo 路径绘制 路径绘制是PathMeasure最常用的功能,其原理就是通过getSegment来不断截取Path片段,从而不断绘制完整的路径,效果如图所示: 代码如下所示: package xys.com.pathart.views; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.util.AttributeSet; import android.view.View; /** * 路径动画 PathMeasure * <p/> * Created by xuyisheng on 16/7/15. */ public class PathPainter extends View { private Path mPath; private Paint mPaint; private PathMeasure mPathMeasure; private float mAnimatorValue; private Path mDst; private float mLength; public PathPainter(Context context) { super(context); } public PathPainter(Context context, AttributeSet attrs) { super(context, attrs); mPathMeasure = new PathMeasure(); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(5); mPath = new Path(); mPath.addCircle(400, 400, 100, Path.Direction.CW); mPathMeasure.setPath(mPath, true); mLength = mPathMeasure.getLength(); mDst = new Path(); final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mAnimatorValue = (float) valueAnimator.getAnimatedValue(); invalidate(); } }); valueAnimator.setDuration(2000); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.start(); } public PathPainter(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mDst.reset(); // 硬件加速的BUG mDst.lineTo(0,0); float stop = mLength * mAnimatorValue; mPathMeasure.getSegment(0, stop, mDst, true); canvas.drawPath(mDst, mPaint); } } 通过这种方式,只需要做一点点小的修改,就可以完成一个比较有意思的loading图,效果如下所示: 我们只需要修改下起始值的数字即可,关键代码如下: @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mDst.reset(); // 硬件加速的BUG mDst.lineTo(0,0); float stop = mLength * mAnimatorValue; float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * mLength)); mPathMeasure.getSegment(start, stop, mDst, true); canvas.drawPath(mDst, mPaint); } 路径绘制——另辟蹊径 关于路径绘制,View的始祖Romain Guy曾经有一篇文章讲解了一个很使用的技巧,地址如下所示: http://www.curious-creature.com/2013/12/21/android-recipe-4-path-tracing/ Romain Guy使用DashPathEffect来实现了路径绘制。 DashPathEffect(float[] intervals, float phase) DashPathEffect传入了一个intervals数组,用来控制实线和虚线的数组的显示,那么当实线和虚线都是整个路径的长度时,整个路径就只显示实线或者虚线了,这时候通过第二个参数phase来控制起始偏移量,就可以完成整个路径的绘制了,这的确是一个非常trick而且有效的方式,效果如图所示: 代码如下所示: package xys.com.pathart.views; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathEffect; import android.graphics.PathMeasure; import android.util.AttributeSet; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; /** * 路径绘制——DashPathEffect * <p/> * Created by xuyisheng on 16/7/15. */ public class PathPainterEffect extends View implements View.OnClickListener{ private Paint mPaint; private Path mPath; private PathMeasure mPathMeasure; private PathEffect mEffect; private float fraction = 0; private ValueAnimator mAnimator; public PathPainterEffect(Context context) { super(context); } public PathPainterEffect(Context context, AttributeSet attrs) { super(context, attrs); mPath = new Path(); mPath.reset(); mPath.moveTo(100, 100); mPath.lineTo(100, 500); mPath.lineTo(400, 300); mPath.close(); mPaint = new Paint(); mPaint.setColor(Color.BLACK); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(5); mPathMeasure = new PathMeasure(mPath, false); final float length = mPathMeasure.getLength(); mAnimator = ValueAnimator.ofFloat(1, 0); mAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); mAnimator.setDuration(2000); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { fraction = (float) valueAnimator.getAnimatedValue(); mEffect = new DashPathEffect(new float[]{length, length}, fraction * length); mPaint.setPathEffect(mEffect); invalidate(); } }); setOnClickListener(this); } public PathPainterEffect(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawPath(mPath, mPaint); } @Override public void onClick(View view) { mAnimator.start(); } } 其关键代码就是在于设置: mEffect = new DashPathEffect(new float[]{length, length}, fraction * length); 后面通过属性动画来控制路径绘制即可。 坐标与切线 PathMeasure的getPosTan()方法,可以获取路径上的坐标点和对应点的切线坐标,其中,路径上对应的点非常好理解,就是对应的点的坐标,而另一个参数tan[]数组,它用于返回当前点的运动轨迹的斜率,要理解这个API,我们首先来看下Math中的atan2这个方法: public static double atan2 (double y, double x) 虽然atan()方法可以用于求一个反正切值,但是他传入的是一个角度,所以我们使用atan2()方法: Math.atan2()函数返回点(x,y)和原点(0,0)之间直线的倾斜角 那么如何计算任意两点间直线的倾斜角呢?只需要将两点x,y坐标分别相减得到一个新的点(x2-x1,y2-y1)。然后利用它求出角度即可——Math.atan2(y2-y1,x2-x1)。 利用这个API,通常可以获取Path上的点坐标和点的运动趋势,对于运动趋势,通常通过Math.atan2()来转换为切线的角度,代码如下所示: @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mMeasure.getPosTan(mMeasure.getLength() * currentValue, pos, tan); float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); } 根据这个API,我们可以模拟一个圆上的点和点的运动趋势,代码如下: package xys.com.pathart.views; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.util.AttributeSet; import android.view.View; /** * 曲线上切点 * <p/> * Created by xuyisheng on 16/7/15. */ public class PathTan extends View implements View.OnClickListener { private Path mPath; private float[] pos; private float[] tan; private Paint mPaint; float currentValue = 0; private PathMeasure mMeasure; public PathTan(Context context) { super(context); } public PathTan(Context context, AttributeSet attrs) { super(context, attrs); mPath = new Path(); mPaint = new Paint(); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(4); mMeasure = new PathMeasure(); mPath.addCircle(0, 0, 200, Path.Direction.CW); mMeasure.setPath(mPath, false); pos = new float[2]; tan = new float[2]; setOnClickListener(this); } public PathTan(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mMeasure.getPosTan(mMeasure.getLength() * currentValue, pos, tan); float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); canvas.save(); canvas.translate(400, 400); canvas.drawPath(mPath, mPaint); canvas.drawCircle(pos[0], pos[1], 10, mPaint); canvas.rotate(degrees); canvas.drawLine(0, -200, 300, -200, mPaint); canvas.restore(); } @Override public void onClick(View view) { ValueAnimator animator = ValueAnimator.ofFloat(0, 1); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { currentValue = (float) valueAnimator.getAnimatedValue(); invalidate(); } }); animator.setDuration(3000); animator.setRepeatCount(ValueAnimator.INFINITE); animator.start(); } } Demo效果如图所示: 只不过这里在绘制的时候,使用了一些Trick,先通过canvas.translate方法将原点移动的圆心,同时,通过canvas.rotate将运动趋势的角度转换为画布的旋转,这样每次绘制切线,就只需要画一条同样的切线即可。 源代码 源码已上传到Github: https://github.com/xuyisheng/PathArt
贝塞尔曲线开发的艺术 一句话概括贝塞尔曲线:将任意一条曲线转化为精确的数学公式。 很多绘图工具中的钢笔工具,就是典型的贝塞尔曲线的应用,这里的一个网站可以在线模拟钢笔工具的使用: http://bezier.method.ac/ 贝塞尔曲线中有一些比较关键的名词,解释如下: 数据点:通常指一条路径的起始点和终止点 控制点:控制点决定了一条路径的弯曲轨迹,根据控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲线(1个控制点)、三阶贝塞尔曲线(2个控制点)等等。 要想对贝塞尔曲线有一个比较好的认识,可以参考WIKI上的链接: https://en.wikipedia.org/wiki/B%C3%A9zier_curve 贝塞尔曲线模拟 在Android中,一般来说,开发者只考虑二阶贝塞尔曲线和三阶贝塞尔曲线,SDK也只提供了二阶和三阶的API调用。对于再高阶的贝塞尔曲线,通常可以将曲线拆分成多个低阶的贝塞尔曲线,也就是所谓的降阶操作。下面将通过代码来模拟二阶和三阶的贝塞尔曲线是如何绘制和控制的。 贝塞尔曲线的一个比较好的动态演示如下所示: http://myst729.github.io/bezier-curve/ 二阶模拟 二阶贝塞尔曲线在Android中的API为:quadTo()和rQuadTo(),这两个API在原理上是可以互相转换的——quadTo是基于绝对坐标,而rQuadTo是基于相对坐标,所以后面我都只以其中一个来进行讲解。 先来看下最终的效果: 从前面的介绍可以知道,二阶贝塞尔曲线有两个数据点和一个控制点,只需要在代码中绘制出这些辅助点和辅助线即可,同时,控制点可以通过onTouchEvent来进行传递。 package com.xys.animationart.views; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; /** * 二阶贝塞尔曲线 * <p/> * Created by xuyisheng on 16/7/11. */ public class SecondOrderBezier extends View { private Paint mPaintBezier; private Paint mPaintAuxiliary; private Paint mPaintAuxiliaryText; private float mAuxiliaryX; private float mAuxiliaryY; private float mStartPointX; private float mStartPointY; private float mEndPointX; private float mEndPointY; private Path mPath = new Path(); public SecondOrderBezier(Context context) { super(context); } public SecondOrderBezier(Context context, AttributeSet attrs) { super(context, attrs); mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBezier.setStyle(Paint.Style.STROKE); mPaintBezier.setStrokeWidth(8); mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintAuxiliary.setStyle(Paint.Style.STROKE); mPaintAuxiliary.setStrokeWidth(2); mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintAuxiliaryText.setStyle(Paint.Style.STROKE); mPaintAuxiliaryText.setTextSize(20); } public SecondOrderBezier(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mStartPointX = w / 4; mStartPointY = h / 2 - 200; mEndPointX = w / 4 * 3; mEndPointY = h / 2 - 200; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); mPath.moveTo(mStartPointX, mStartPointY); // 辅助点 canvas.drawPoint(mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary); canvas.drawText("控制点", mAuxiliaryX, mAuxiliaryY, mPaintAuxiliaryText); canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText); canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText); // 辅助线 canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary); canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary); // 二阶贝塞尔曲线 mPath.quadTo(mAuxiliaryX, mAuxiliaryY, mEndPointX, mEndPointY); canvas.drawPath(mPath, mPaintBezier); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: mAuxiliaryX = event.getX(); mAuxiliaryY = event.getY(); invalidate(); } return true; } } 三阶模拟 三阶贝塞尔曲线在Android中的API为:cubicTo()和rCubicTo(),这两个API在原理上是可以互相转换的——quadTo是基于绝对坐标,而rCubicTo是基于相对坐标,所以后面我都只以其中一个来进行讲解。 有了二阶的基础,再来模拟三阶就非常简单了,无非是增加了一个控制点而已,先看下效果图: 代码只需要在二阶的基础上添加一些辅助点即可,下面只给出一些关键代码,详细代码请参考Github: @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); mPath.moveTo(mStartPointX, mStartPointY); // 辅助点 canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary); canvas.drawText("控制点1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText); canvas.drawText("控制点2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText); canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText); canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText); // 辅助线 canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary); canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary); canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary); // 三阶贝塞尔曲线 mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY); canvas.drawPath(mPath, mPaintBezier); } 模拟网页 如下所示的网页,模拟了三阶贝塞尔曲线的绘制,可以通过拖动曲线来获取两个控制点的坐标,而起始点分别是(0,0)和(1,1)。 http://cubic-bezier.com/ 通过这个网页,也可以比较方便的获取三阶贝塞尔曲线的控制点坐标。 贝塞尔曲线应用 圆滑绘图 当在屏幕上绘制路径时,例如手写板,最基本的方法是通过Path.lineTo将各个触点连接起来,而这种方式在很多时候会发现,两个点的连接是非常生硬的,因为它毕竟是通过直线来连接的,如果通过二阶贝塞尔曲线来将各个触点连接,就会圆滑的多,不会出现太多的生硬连接。 先来看下代码,非常简单的绘制路径代码: package com.xys.animationart.views; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; /** * 圆滑路径 * <p/> * Created by xuyisheng on 16/7/19. */ public class DrawPadBezier extends View { private float mX; private float mY; private float offset = ViewConfiguration.get(getContext()).getScaledTouchSlop(); private Paint mPaint; private Path mPath; public DrawPadBezier(Context context) { super(context); } public DrawPadBezier(Context context, AttributeSet attrs) { super(context, attrs); mPath = new Path(); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(5); mPaint.setColor(Color.RED); } public DrawPadBezier(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPath.reset(); float x = event.getX(); float y = event.getY(); mX = x; mY = y; mPath.moveTo(x, y); break; case MotionEvent.ACTION_MOVE: float x1 = event.getX(); float y1 = event.getY(); float preX = mX; float preY = mY; float dx = Math.abs(x1 - preX); float dy = Math.abs(y1 - preY); if (dx >= offset || dy >= offset) { // 贝塞尔曲线的控制点为起点和终点的中点 float cX = (x1 + preX) / 2; float cY = (y1 + preY) / 2; // mPath.quadTo(preX, preY, cX, cY); mPath.lineTo(x1, y1); mX = x1; mY = y1; } } invalidate(); return true; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawPath(mPath, mPaint); } } 先来看下通过mPath.lineTo来实现的绘图,效果如下所示: 图片中的拐点有明显的锯齿效果,即通过直线的连接,再来看下通过贝塞尔曲线来连接的效果,通常情况下,贝塞尔曲线的控制点取两个连续点的中点: mPath.quadTo(preX, preY, cX, cY); 通过二阶贝塞尔曲线的连接效果如图所示: 可以明显的发现,曲线变得更加圆滑了。 曲线变形 通过控制贝塞尔曲线的控制点,就可以实现对一条路径的修改。所以,利用贝塞尔曲线,可以实现很多的路径动画,例如: package com.xys.animationart; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.View; import android.view.animation.BounceInterpolator; /** * 曲线变形 * <p/> * Created by xuyisheng on 16/7/11. */ public class PathMorphBezier extends View implements View.OnClickListener{ private Paint mPaintBezier; private Paint mPaintAuxiliary; private Paint mPaintAuxiliaryText; private float mAuxiliaryOneX; private float mAuxiliaryOneY; private float mAuxiliaryTwoX; private float mAuxiliaryTwoY; private float mStartPointX; private float mStartPointY; private float mEndPointX; private float mEndPointY; private Path mPath = new Path(); private ValueAnimator mAnimator; public PathMorphBezier(Context context) { super(context); } public PathMorphBezier(Context context, AttributeSet attrs) { super(context, attrs); mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBezier.setStyle(Paint.Style.STROKE); mPaintBezier.setStrokeWidth(8); mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintAuxiliary.setStyle(Paint.Style.STROKE); mPaintAuxiliary.setStrokeWidth(2); mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintAuxiliaryText.setStyle(Paint.Style.STROKE); mPaintAuxiliaryText.setTextSize(20); setOnClickListener(this); } public PathMorphBezier(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mStartPointX = w / 4; mStartPointY = h / 2 - 200; mEndPointX = w / 4 * 3; mEndPointY = h / 2 - 200; mAuxiliaryOneX = mStartPointX; mAuxiliaryOneY = mStartPointY; mAuxiliaryTwoX = mEndPointX; mAuxiliaryTwoY = mEndPointY; mAnimator = ValueAnimator.ofFloat(mStartPointY, (float) h); mAnimator.setInterpolator(new BounceInterpolator()); mAnimator.setDuration(1000); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mAuxiliaryOneY = (float) valueAnimator.getAnimatedValue(); mAuxiliaryTwoY = (float) valueAnimator.getAnimatedValue(); invalidate(); } }); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); mPath.moveTo(mStartPointX, mStartPointY); // 辅助点 canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary); canvas.drawText("辅助点1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText); canvas.drawText("辅助点2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText); canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText); canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText); // 辅助线 canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary); canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary); canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary); // 三阶贝塞尔曲线 mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY); canvas.drawPath(mPath, mPaintBezier); } @Override public void onClick(View view) { mAnimator.start(); } } 这里就是简单的改变二阶贝塞尔曲线的控制点来实现曲线的变形。 网上一些比较复杂的变形动画效果,也是基于这种实现方式,其原理都是通过改变控制点的位置,从而达到对图形的变换,例如圆形到心形的变化、圆形到五角星的变换,等等。 波浪效果 波浪的绘制是贝塞尔曲线一个非常简单的应用,而让波浪进行波动,其实并不需要对控制点进行改变,而是可以通过位移来实现,这里我们是借助贝塞尔曲线来实现波浪的绘制效果,效果如图所示: package com.xys.animationart.views; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.View; import android.view.animation.LinearInterpolator; /** * 波浪图形 * <p/> * Created by xuyisheng on 16/7/11. */ public class WaveBezier extends View implements View.OnClickListener { private Paint mPaint; private Path mPath; private int mWaveLength = 1000; private int mOffset; private int mScreenHeight; private int mScreenWidth; private int mWaveCount; private int mCenterY; public WaveBezier(Context context) { super(context); } public WaveBezier(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public WaveBezier(Context context, AttributeSet attrs) { super(context, attrs); mPath = new Path(); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.LTGRAY); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); setOnClickListener(this); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mScreenHeight = h; mScreenWidth = w; mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5); mCenterY = mScreenHeight / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); mPath.moveTo(-mWaveLength + mOffset, mCenterY); for (int i = 0; i < mWaveCount; i++) { // + (i * mWaveLength) // + mOffset mPath.quadTo((-mWaveLength * 3 / 4) + (i * mWaveLength) + mOffset, mCenterY + 60, (-mWaveLength / 2) + (i * mWaveLength) + mOffset, mCenterY); mPath.quadTo((-mWaveLength / 4) + (i * mWaveLength) + mOffset, mCenterY - 60, i * mWaveLength + mOffset, mCenterY); } mPath.lineTo(mScreenWidth, mScreenHeight); mPath.lineTo(0, mScreenHeight); mPath.close(); canvas.drawPath(mPath, mPaint); } @Override public void onClick(View view) { ValueAnimator animator = ValueAnimator.ofInt(0, mWaveLength); animator.setDuration(1000); animator.setRepeatCount(ValueAnimator.INFINITE); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mOffset = (int) animation.getAnimatedValue(); postInvalidate(); } }); animator.start(); } } 波浪动画实际上并不复杂,但三角函数确实对一些开发者比较困难,开发者可以通过下面的这个网站来模拟三角函数图像的绘制: https://www.desmos.com/calculator 路径动画 贝塞尔曲线的另一个非常常用的功能,就是作为动画的运动轨迹,让动画目标能够沿曲线平滑的实现移动动画,也就是让物体沿着贝塞尔曲线运动,而不是机械的直线,本例实现效果如下所示: package com.xys.animationart.views; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.util.AttributeSet; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import com.xys.animationart.evaluator.BezierEvaluator; /** * 贝塞尔路径动画 * <p/> * Created by xuyisheng on 16/7/12. */ public class PathBezier extends View implements View.OnClickListener { private Paint mPathPaint; private Paint mCirclePaint; private int mStartPointX; private int mStartPointY; private int mEndPointX; private int mEndPointY; private int mMovePointX; private int mMovePointY; private int mControlPointX; private int mControlPointY; private Path mPath; public PathBezier(Context context) { super(context); } public PathBezier(Context context, AttributeSet attrs) { super(context, attrs); mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPathPaint.setStyle(Paint.Style.STROKE); mPathPaint.setStrokeWidth(5); mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mStartPointX = 100; mStartPointY = 100; mEndPointX = 600; mEndPointY = 600; mMovePointX = mStartPointX; mMovePointY = mStartPointY; mControlPointX = 500; mControlPointY = 0; mPath = new Path(); setOnClickListener(this); } public PathBezier(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); canvas.drawCircle(mStartPointX, mStartPointY, 30, mCirclePaint); canvas.drawCircle(mEndPointX, mEndPointY, 30, mCirclePaint); mPath.moveTo(mStartPointX, mStartPointY); mPath.quadTo(mControlPointX, mControlPointY, mEndPointX, mEndPointY); canvas.drawPath(mPath, mPathPaint); canvas.drawCircle(mMovePointX, mMovePointY, 30, mCirclePaint); } @Override public void onClick(View view) { BezierEvaluator bezierEvaluator = new BezierEvaluator(new PointF(mControlPointX, mControlPointY)); ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator, new PointF(mStartPointX, mStartPointY), new PointF(mEndPointX, mEndPointY)); anim.setDuration(600); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { PointF point = (PointF) valueAnimator.getAnimatedValue(); mMovePointX = (int) point.x; mMovePointY = (int) point.y; invalidate(); } }); anim.setInterpolator(new AccelerateDecelerateInterpolator()); anim.start(); } } 其中,用于改变运动点坐标的关键evaluator如下所示: package com.xys.animationart.evaluator; import android.animation.TypeEvaluator; import android.graphics.PointF; import com.xys.animationart.util.BezierUtil; public class BezierEvaluator implements TypeEvaluator<PointF> { private PointF mControlPoint; public BezierEvaluator(PointF controlPoint) { this.mControlPoint = controlPoint; } @Override public PointF evaluate(float t, PointF startValue, PointF endValue) { return BezierUtil.CalculateBezierPointForQuadratic(t, startValue, mControlPoint, endValue); } } 这里的TypeEvaluator计算用到了计算贝塞尔曲线上点的计算算法,这个会在后面继续讲解。 贝塞尔曲线进阶 求贝塞尔曲线上任意一点的坐标 求贝塞尔曲线上任意一点的坐标,这一过程,就是利用了De Casteljau算法。 http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/de-casteljau.html 利用这一算法,有开发者开发了一个演示多阶贝塞尔曲线的效果的App,其原理就是通过绘制贝塞尔曲线上的点来进行绘制的,地址如下所示: https://github.com/venshine/BezierMaker 下面这篇文章就详细的讲解了该算法的应用,我的代码也从这里提取而来: http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/ 计算 有了公式,只需要代码实现就OK了,我们先写两个公式: package com.xys.animationart.util; import android.graphics.PointF; /** * 计算贝塞尔曲线上的点坐标 * <p/> * Created by xuyisheng on 16/7/13. */ public class BezierUtil { /** * B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1] * * @param t 曲线长度比例 * @param p0 起始点 * @param p1 控制点 * @param p2 终止点 * @return t对应的点 */ public static PointF CalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) { PointF point = new PointF(); float temp = 1 - t; point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x; point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y; return point; } /** * B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1] * * @param t 曲线长度比例 * @param p0 起始点 * @param p1 控制点1 * @param p2 控制点2 * @param p3 终止点 * @return t对应的点 */ public static PointF CalculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) { PointF point = new PointF(); float temp = 1 - t; point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t; point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t; return point; } } 我们来将路径绘制到View中,看是否正确: package com.xys.animationart.views; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.util.AttributeSet; import android.view.View; import com.xys.animationart.util.BezierUtil; /** * 通过计算模拟二阶、三阶贝塞尔曲线 * <p/> * Created by xuyisheng on 16/7/13. */ public class CalculateBezierPointView extends View implements View.OnClickListener { private Paint mPaint; private ValueAnimator mAnimatorQuadratic; private ValueAnimator mAnimatorCubic; private PointF mPointQuadratic; private PointF mPointCubic; public CalculateBezierPointView(Context context) { super(context); } public CalculateBezierPointView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CalculateBezierPointView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mAnimatorQuadratic = ValueAnimator.ofFloat(0, 1); mAnimatorQuadratic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { PointF point = BezierUtil.CalculateBezierPointForQuadratic(valueAnimator.getAnimatedFraction(), new PointF(100, 100), new PointF(500, 100), new PointF(500, 500)); mPointQuadratic.x = point.x; mPointQuadratic.y = point.y; invalidate(); } }); mAnimatorCubic = ValueAnimator.ofFloat(0, 1); mAnimatorCubic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { PointF point = BezierUtil.CalculateBezierPointForCubic(valueAnimator.getAnimatedFraction(), new PointF(100, 600), new PointF(100, 1100), new PointF(500, 1000), new PointF(500, 600)); mPointCubic.x = point.x; mPointCubic.y = point.y; invalidate(); } }); mPointQuadratic = new PointF(); mPointQuadratic.x = 100; mPointQuadratic.y = 100; mPointCubic = new PointF(); mPointCubic.x = 100; mPointCubic.y = 600; setOnClickListener(this); } @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(mPointQuadratic.x, mPointQuadratic.y, 10, mPaint); canvas.drawCircle(mPointCubic.x, mPointCubic.y, 10, mPaint); } @Override public void onClick(View view) { AnimatorSet set = new AnimatorSet(); set.playTogether(mAnimatorQuadratic, mAnimatorCubic); set.setDuration(2000); set.start(); } } 这次我们并没有通过API提供的贝塞尔曲线绘制方法来绘制二阶、三阶贝塞尔曲线,而是通过时间t和起始点来计算一条贝塞尔曲线上的所有点,可以发现,通过算法计算出来的点,与通过API所绘制出来的点,是完全吻合的。 贝塞尔曲线拟合计算 贝塞尔曲线有一个非常常用的动画效果——MetaBall算法。相信很多开发者都见过类似的动画,例如QQ的小红点消除,UC浏览器的下拉刷新loading等等。要做好这个动画,实际上最重要的就是通过贝塞尔曲线来拟合两个图形。 效果如图所示: 矩形拟合 我们来看一下拟合的原理,实际上就是通过贝塞尔曲线来连接两个圆上的四个点,当我们调整下画笔的填充方式,并绘制一些辅助线,我们来看具体是如何进行拟合的,如图所示: 可以发现,控制点为两圆圆心连线的中点,连接线为图中的这样一个矩形,当圆比较小时,这种通过矩形来拟合的方式几乎是没有问题的,但我们把圆放大,再来看下这种拟合,如图所示: 当圆的半径扩大之后,就可以非常明显的发现拟合的连接点与圆有一定相交的区域,这样的拟合效果就不好了,我们将画笔模式调整回来,如图所示: 所以,简单的矩形拟合,在圆半径小的时候,是可以的,但当圆半径变大之后,就需要更加严格的拟合了。 这里我们先来讲解下,如何计算矩形拟合的几个关键点。 从前面那张线图可以看出,标红的两个角是相等的,而这个角可以通过两个圆心的坐标来算出,有了这样一个角度,通过R x cos和 R x sin来计算矩形的一个顶点的坐标,类似的,其它坐标可求,关键代码如下所示: private void metaBallVersion1(Canvas canvas) { float x = mCircleTwoX; float y = mCircleTwoY; float startX = mCircleOneX; float startY = mCircleOneY; float dx = x - startX; float dy = y - startY; double a = Math.atan(dx / dy); float offsetX = (float) (mCircleOneRadius * Math.cos(a)); float offsetY = (float) (mCircleOneRadius * Math.sin(a)); float x1 = startX + offsetX; float y1 = startY - offsetY; float x2 = x + offsetX; float y2 = y - offsetY; float x3 = x - offsetX; float y3 = y + offsetY; float x4 = startX - offsetX; float y4 = startY + offsetY; float controlX = (startX + x) / 2; float controlY = (startY + y) / 2; mPath.reset(); mPath.moveTo(x1, y1); mPath.quadTo(controlX, controlY, x2, y2); mPath.lineTo(x3, y3); mPath.quadTo(controlX, controlY, x4, y4); mPath.lineTo(x1, y1); // 辅助线 canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint); canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint); canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint); canvas.drawLine(x1, y1, x2, y2, mPaint); canvas.drawLine(x3, y3, x4, y4, mPaint); canvas.drawCircle(controlX, controlY, 5, mPaint); canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint); canvas.drawLine(x1, y1, x1, mCircleOneY, mPaint); canvas.drawPath(mPath, mPaint); } 切线拟合 如前面所说,矩形拟合在半径较小的情况下,是可以实现完美拟合的,而当半径变大后,就会出现贝塞尔曲线与圆相交的情况,导致拟合失败。 那么如何来实现完美的拟合呢?实际上,也就是说贝塞尔曲线与圆的连接点到贝塞尔曲线的控制点的连线,一定是圆的切线,这样的话,无论圆的半径如何变化,贝塞尔曲线一定是与圆拟合的,具体效果如图所示: 这时候我们把画笔模式调整回来看下填充效果,如图所示: 这样拟合是非常完美的。那么要如何来计算这些拟合的关键点呢?在前面的线图中,我标记出了两个角,这两个角分别可以求出,相减,就可以获取切点与圆心的夹角了,这样,通过R x cos和R x sin就可以求出切点的坐标了。 其中,小的角可以通过两个圆心的坐标来求出,而大的角,可以通过直角三角形(圆心、切点、控制点)来求出,即控制点到圆心的距离/半径。 关键代码如下所示: private void metaBallVersion2(Canvas canvas) { float x = mCircleTwoX; float y = mCircleTwoY; float startX = mCircleOneX; float startY = mCircleOneY; float controlX = (startX + x) / 2; float controlY = (startY + y) / 2; float distance = (float) Math.sqrt((controlX - startX) * (controlX - startX) + (controlY - startY) * (controlY - startY)); double a = Math.acos(mRadiusNormal / distance); double b = Math.acos((controlX - startX) / distance); float offsetX1 = (float) (mRadiusNormal * Math.cos(a - b)); float offsetY1 = (float) (mRadiusNormal * Math.sin(a - b)); float tanX1 = startX + offsetX1; float tanY1 = startY - offsetY1; double c = Math.acos((controlY - startY) / distance); float offsetX2 = (float) (mRadiusNormal * Math.sin(a - c)); float offsetY2 = (float) (mRadiusNormal * Math.cos(a - c)); float tanX2 = startX - offsetX2; float tanY2 = startY + offsetY2; double d = Math.acos((y - controlY) / distance); float offsetX3 = (float) (mRadiusNormal * Math.sin(a - d)); float offsetY3 = (float) (mRadiusNormal * Math.cos(a - d)); float tanX3 = x + offsetX3; float tanY3 = y - offsetY3; double e = Math.acos((x - controlX) / distance); float offsetX4 = (float) (mRadiusNormal * Math.cos(a - e)); float offsetY4 = (float) (mRadiusNormal * Math.sin(a - e)); float tanX4 = x - offsetX4; float tanY4 = y + offsetY4; mPath.reset(); mPath.moveTo(tanX1, tanY1); mPath.quadTo(controlX, controlY, tanX3, tanY3); mPath.lineTo(tanX4, tanY4); mPath.quadTo(controlX, controlY, tanX2, tanY2); canvas.drawPath(mPath, mPaint); // 辅助线 canvas.drawCircle(tanX1, tanY1, 5, mPaint); canvas.drawCircle(tanX2, tanY2, 5, mPaint); canvas.drawCircle(tanX3, tanY3, 5, mPaint); canvas.drawCircle(tanX4, tanY4, 5, mPaint); canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint); canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint); canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint); canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint); canvas.drawCircle(controlX, controlY, 5, mPaint); canvas.drawLine(startX, startY, tanX1, tanY1, mPaint); canvas.drawLine(tanX1, tanY1, controlX, controlY, mPaint); } 圆的拟合 贝塞尔曲线做动画,很多时候都需要使用到圆的特效,而通过二阶、三阶贝塞尔曲线来拟合圆,也不是一个非常简单的事情,所以,我直接把结论拿出来了,具体的算法地址如下所示: http://spencermortensen.com/articles/bezier-circle/ http://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves 有了贝塞尔曲线的控制点,再对其实现动画,就非常简单了,与之前的动画没有太大的区别。 源代码 本次的讲解代码已经全部上传到Github : https://github.com/xuyisheng/BezierArt 欢迎大家提issue。
Android Vector曲折的兼容之路 两年前写书的时候,就在研究Android L提出的Vector,可研究下来发现,完全不具备兼容性,相信这也是它没有被广泛使用的一个原因,经过Google的不懈努力,现在Vector终于迎来了它的春天。 在文章后面,会给出本文的Demo和效果图,并开源在Github Vector Drawable Android 5.0发布的时候,Google提供了Vector的支持。Vector Drawable相对于普通的Drawable来说,有以下几个好处: Vector图像可以自动进行适配,不需要通过分辨率来设置不同的图片 Vector图像可以大幅减少图像的体积,同样一张图,用Vector来实现,可能只有PNG的几十分之一 使用简单,很多设计工具,都可以直接导出SVG图像,从而转换成Vector图像 功能强大,不用写很多代码就可以实现非常复杂的动画 成熟、稳定,前端已经非常广泛的进行使用了 Vector图像刚发布的时候,是只支持Android 5.0+的,对于Android pre-L的系统来说,并不能使用,所以,可以说那时候的Vector并没有什么卵用。不过自从AppCompat 23.2之后,Google对p-View的Android系统也进行了兼容,也就是说,Vector可以使用于Android 2.1以上的所有系统,只需要引用com.android.support:appcompat-v7:23.2.0以上的版本就可以了,这时候,Vector应该算是迎来了它的春天。 如何获得Vector图像 概念 首先,需要讲解两个概念——SVG和Vector。 SVG,即Scalable Vector Graphics 矢量图,这种图像格式在前端中已经使用的非常广泛了,详见WIKI:https://en.wikipedia.org/wiki/Scalable_Vector_Graphics Vector,在Android中指的是Vector Drawable,也就是Android中的矢量图,详见:https://developer.android.com/reference/android/graphics/drawable/VectorDrawable.html 因此,可以说Vector就是Android中的SVG实现,因为Android中的Vector并不是支持全部的SVG语法,也没有必要,因为完整的SVG语法是非常复杂的,但已经支持的SVG语法已经够用了,特别是Path语法,几乎是Android中Vector的标配,详细可以参考:http://www.w3.org/TR/SVG/paths.html Vector语法简介 Android以一种简化的方式对SVG进行了兼容,这种方式就是通过使用它的Path标签,通过Path标签,几乎可以实现SVG中的其它所有标签,虽然可能会复杂一点,但这些东西都是可以通过工具来完成的,所以,不用担心写起来会很复杂。 Path指令解析如下所示: 支持的指令: M = moveto(M X,Y) :将画笔移动到指定的坐标位置 L = lineto(L X,Y) :画直线到指定的坐标位置 H = horizontal lineto(H X):画水平线到指定的X坐标位置 V = vertical lineto(V Y):画垂直线到指定的Y坐标位置 C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次贝赛曲线 S = smooth curveto(S X2,Y2,ENDX,ENDY) Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次贝赛曲线 T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射 A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线 Z = closepath():关闭路径 使用原则: 坐标轴为以(0,0)为中心,X轴水平向右,Y轴水平向下 所有指令大小写均可。大写绝对定位,参照全局坐标系;小写相对定位,参照父容器坐标系 指令和数据间的空格可以省略 同一指令出现多次可以只用一个 注意,’M’处理时,只是移动了画笔, 没有画任何东西。 它也可以在后面给出上同时绘制不连续线。 关于这些语法,开发者需要的并不是全部精通,而是能够看懂即可,其它的都可以交给工具来实现。 从PNG到SVG 设计师 要从一般使用的PNG图像转换到SVG图像,对于设计师来说,并不是一件难事,因为大部分的设计工具(PS、Illustrator等等)都支持导出各种格式的图像,如PNG、JPG,当然,也包括SVG,因此,设计师可以完全按照原有的方式进行设计,只是最后导出的时候,选择SVG即可。 程序员 不要求开发者都去学习使用这些设计工具,开发者可以利用一些工具,自己转换一些比较基础的图像,http://inloop.github.io/svg2android/ 就是这样一个非常牛逼的网站,可以在线将普通图像转换为Android Vector Drawable。如图所示: 或者,还可以使用SVG的编辑器来进行SVG图像的编写,例如http://editor.method.ac/ 使用Android Studio 利用Android Studio的Vector Asset,可以非常方便的创建Vector图像,甚至可以直接通过本地的SVG图像来生成Vector图像,如图所示: 进去之后,就可以生成Vector图像,如图所示: Google的兼容之路 只兼容L+ Vector是在Android L中提出来的新概念,所以在刚开始的时候是只兼容L+的。 Gradle Plugin 1.5的兼容 从Gradle Plugin 1.5开始,Google支持了一种兼容方式,即在Android L之上,使用Vector,而在L之下,则使用Gradle将Vector生成PNG图像。 Android gradle plugin 1.5发布以后,加入了一个跟VectorDrawable有关的新功能。Android build tools 提供了另外一种解决兼容性的方案,如果编译的版本是5.0之前的版本,那么build tools 会把VectorDrawable生成对应的png图片,这样在5.0以下的版本则使用的是生成的png图,而在5.0以上的版本中则使用VectorDrawable.在build.gradle添加generatedDensities配置,可以配置生成的png图片的密度。 AppCompat23.2的兼容 从AppCompat23.2开始,Google开始支持在低版本上使用Vector。 静态Vector图像 我们有很多方法能够得到这些Vector,那么如何使用它们呢,Android 5.0以上的使用就不讲了,不太具有普遍代表性,我们从pre-L版本的兼容开始做起。 pre-L版本兼容 VectorDrawableCompat依赖于AAPT的一些功能,它能保持最近矢量图使用的添加的属性ID,以便他们可以被pre-L版本之前的引用。 在Android 5.0之前使用Vector,需要aapt来对资源进行一些处理,这一过程可以在aapt的配置中进行设置,如果没有启用这样一个flag,那么在5.0以下的设备上运行就会发生android.content.res.Resources$NotFoundException。 首先,你需要在项目的build.gradle脚本中,增加对Vector兼容性的支持,代码如下所示: 使用Gradle Plugin 2.0以上: android { defaultConfig { vectorDrawables.useSupportLibrary = true } } 使用Gradle Plugin 2.0以下,Gradle Plugin 1.5以上: android { defaultConfig { // Stops the Gradle plugin’s automatic rasterization of vectors generatedDensities = [] } // Flag to tell aapt to keep the attribute ids around aaptOptions { additionalParameters "--no-version-vectors" } } 像前面提到的,这种兼容方式实际上是先关闭AAPT对pre-L版本使用Vector的妥协,即在L版本以上,使用Vector,而在pre-L版本上,使用Gradle生成相应的PNG图片,generatedDensities这个数组,实际上就是要生成PNG的图片分辨率的数组,使用appcompat后就不需要这样了。 当然,最重要的还是添加appcompat的支持: compile 'com.android.support:appcompat-v7:23.4.0' 同时,确保你使用的是AppCompatActivity而不是普通的Activity。 Vector图像 一个基本的Vector图像,实际上也是一个xml文件,如下所示: <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="200dp" android:height="200dp" android:viewportHeight="500" android:viewportWidth="500"> <path android:name="square" android:fillColor="#000000" android:pathData="M100,100 L400,100 L400,400 L100,400 z"/> </vector> 显示如图所示: 这里需要解释下这里的几个标签: android:width \ android:height:定义图片的宽高 android:viewportHeight \ android:viewportWidth:定义图像被划分的比例大小,例如例子中的500,即把200dp大小的图像划分成500份,后面Path标签中的坐标,就全部使用的是这里划分后的坐标系统。 这样做有一个非常好的作用,就是将图像大小与图像分离,后面可以随意修改图像大小,而不需要修改PathData中的坐标。 android:fillColor:PathData中的这些属性就不详细讲了,与Canvas绘图的属性基本类似。 在控件中使用 有了静态的Vector图像,就可以在控件中使用了。 可以发现,这里我们使用的都是普通的ImageView,好像并不是AppcomatImageView,这是因为使用了Appcomat后,系统会自动把ImageView转换为AppcomatImageView。 ImageView\ImageButton 对于ImageView这样的控件,要兼容Vector图像,只需要将之前的android:src属性,换成app:srcCompat即可,示例代码如下所示: <ImageView android:id="@+id/iv" android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/vector_image"/> 在代码中设置的话,代码如下所示: ImageView iv = (ImageView) findViewById(R.id.iv); iv.setImageResource(R.drawable.vector_image); setBackgroundResource也是可以设置Vector的API Button Button并不能直接使用app:srcCompat来使用Vector图像,需要通过Selector来进行使用,首先,创建两个图像,用于Selector的两个状态,代码如下所示: selector1.xml <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> <path android:fillColor="#FF000000" android:pathData="M14.59,8L12,10.59 9.41,8 8,9.41 10.59,12 8,14.59 9.41,16 12,13.41 14.59,16 16,14.59 13.41,12 16,9.41 14.59,8zM12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/> </vector> selector2.xml <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> <path android:fillColor="#FF000000" android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/> </vector> selector.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/selector1" android:state_pressed="true"/> <item android:drawable="@drawable/selector2"/> </selector> 非常简单,只是把普通的Selector中的图像换成了Vector图像而已,接下来,在Button中使用这个Selector即可: <Button android:id="@+id/btn" android:layout_width="70dp" android:layout_height="70dp" android:background="@drawable/selector"/> 然后运行,如果你认为可以运行,那就是太天真了,都说了是兼容,怎么能没有坑呢,这里就是一个坑…… 这个坑实际上是有历史渊源的,Google的一位开发者在博客中写到: First up, this functionality was originally released in 23.2.0, but then we found some memory usage and Configuration updating issues so we it removed in 23.3.0. In 23.4.0 (technically a fix release) we’ve re-added the same functionality but behind a flag which you need to manually enable. 实际上,他们的这个改动,就影响了类似DrawableContainers(DrawableContainers which reference other drawables resources which contain only a vector resource)这样的类,它的一个典型,就是Selector(StateListDrawable也是)。这个开发者在文中提到的flag,就是下面的这段代码,放在Activity的前面就可以了: static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); } 开启这个flag后,你就可以正常使用Selector这样的DrawableContainers了。同时,你还开启了类似android:drawableLeft这样的compound drawable的使用权限,以及RadioButton的使用权限,以及ImageView’s src属性。 RadioButton RadioButton的Button同样可以定义,代码如下所示: <RadioButton android:layout_width="50dp" android:layout_height="50dp" android:button="@drawable/selector"/> 动态Vector基础 动态Vector才是Android Vector Drawable的精髓所在 动态的Vector需要通过animated-vector标签来进行实现,它就像一个粘合剂,将控件与Vector图像粘合在了一起,一个基础的animated-vector代码如下所示: <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/XXXXX1"> <target android:name="left" android:animation="@animator/XXXXX2"/> </animated-vector> 实际上这里面只有两个重点是需要关注的,XXXXX1和XXXXX2。一个具体的示例如下所示: <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/ic_arrow"> <target android:name="left" android:animation="@animator/anim_left"/> <target android:name="right" android:animation="@animator/anim_right"/> </animated-vector> 这里表示目标图像是drawable/ic_arrow,对left、right分别使用了anim_left、anim_right动画。这里的name属性,就是在静态Vector图像中group或者path标签的name属性。 animated-vector标签在现在的Android Studio中实际上是会报错的,但这个并不影响编译和运行,属于Android Studio的Bug。 目标图像 XXXXX1是目标Vector图像,也就是静态的Vector图像,例如: <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="120dp" android:height="120dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> <group android:name="left"> <path android:fillColor="#FF000000" android:pathData="M9.01,14L2,14v2h7.01v3L13,15l-3.99,-4v3"/> </group> <group android:name="right"> <path android:fillColor="#FF000000" android:pathData="M14.99,13v-3L22,10L22,8h-7.01L14.99,5L11,9l3.99,4"/> </group> </vector> 可以发现,这里的Vector图像比之前我们看见的要多了一个group标签。group标签的作用有两个: 对Path进行分组,由于我们后面需要针对Path进行动画,所以可以让具有同样动画效果的Path在同一个Group中 拓展动画效果,单个的path标签是没有translateX和translateY属性的,因此无法使用属性动画来控制path translateY,而group标签是有的,所以我们需要先将相关的path标签元素包裹在一个个的group标签中. 动画效果 XXXXX2实际上就是模板要实现的动画,动画效果实际上就是基础的属性动画,例如: anim_left.xml <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:interpolator="@android:interpolator/anticipate_overshoot" android:propertyName="translateX" android:repeatCount="infinite" android:repeatMode="reverse" android:valueFrom="0" android:valueTo="-10" android:valueType="floatType"/> anim_right.xml <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:interpolator="@android:interpolator/anticipate_overshoot" android:propertyName="translateX" android:repeatCount="infinite" android:repeatMode="reverse" android:valueFrom="0" android:valueTo="10" android:valueType="floatType"/> 在代码中使用 ImageView imageView = (ImageView) findViewById(R.id.iv); AnimatedVectorDrawableCompat animatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create( this, R.drawable.square_anim ); imageView.setImageDrawable(animatedVectorDrawableCompat); ((Animatable) imageView.getDrawable()).start(); 动态Vector兼容性问题 向下兼容问题 一说到兼容,就不得不提到坑,几乎所有的为了兼容而做的改动,都会留下一些不可填满的坑,动态Vector动画也不例外,虽然Google已经对Vector图像进行了Android 2.1以上的兼容,但对于动态Vector动画,还是有很多限制的,例如: Path Morphing,即路径变换动画,在Android pre-L版本下是无法使用的。 Path Interpolation,即路径插值器,在Android pre-L版本只能使用系统的插值器,不能自定义。 Path Animation,即路径动画,这个一般使用贝塞尔曲线来代替,所以没有太大影响。 向上兼容问题 除了在低版本上的兼容性问题,在L版本以上,也存在兼容性问题,即继承了AppCompatActivity的界面,如果直接设置ImageView的srcCompat,那么Path Morphing动画是无法生效的,因为默认的AppCompatActivity已经默认使用ImageViewCompat给转换了,但是AnimatedVectorDrawableCompat是不支持Path Morphing动画的,所以,在AppCompatActivity界面里面就无效了。 解决办法很简单,即使用代码来给ImageView添加动画: ImageView imageView = (ImageView) view; AnimatedVectorDrawable morphing = (AnimatedVectorDrawable) getDrawable(morphing); imageView.setImageDrawable(morphing); if (morphing != null) { morphing.start(); } 注意不要使用AnimatedVectorDrawableCompat即可。 抽取string兼容问题 开发者有时候为了代码简洁可能会把Vector图像中的pathData放到string.xml中,然后在Vector图像中引用string。 但这种方式如果通过生成png来兼容5.0以下机型的话,会报pathData错误,编译器不会去读取string.xml,只能把pathData写到Vector图像中,动画文件中也是一样,这也是为了兼容做出的牺牲吗,不得而知。 其它兼容问题 其它非常奇怪、诡异、不能理解的兼容性问题,只能通过版本文件夹的方式来进行兼容了,例如drawable-v21和drawable,分别创建两个文件名相同的资源在两个文件夹下,这样在21以上版本,会使用drawable-v21的资源,而其它会使用drawable下的资源。 动态Vector进阶 用好ObjectAnimator 所谓Vector动画进阶,实际上就是在利用ObjectAnimator的一些属性,特别是trimPathStart、trimPathEnd这两个针对Vector的属性(要注意pathData属性不兼容pre-L)。 这两个属性的官方文档如下所示: android:trimPathStart The fraction of the path to trim from the start, in the range from 0 to 1. android:trimPathEnd The fraction of the path to trim from the end, in the range from 0 to 1. android:trimPathOffset Shift trim region (allows showed region to include the start and end), in the range from 0 to 1. 其实很简单,就是一个图像的截取,设置一个比例即可,即当前绘制多少比例的图像,其余部分不绘制,Start和End分别就是从PathData的Start和End开始算,大家参考几个例子就能理解了。 理解Path Morph Path Morph动画是Vector动画的一个高级使用,说到底,也就是两个PathData的转换,但是这种转换并不是随心所欲的,对于两个PathData,它们能进行Path Morph的前提是,它们具有相同个数的关键点,即两个路径的变换,只是关键点的坐标变化,掌握了这一个基本原理,实现Path Morph就非常容易了。 学习Vector 在Github上我开源了一个Vector的动画Demo库,地址如下所示: https://github.com/xuyisheng/VectorDemo 这个Demo分为两部分,一部分是可以兼容Android pre-L版本和L+版本的Vector动画,另一部分(通过Actionbar的按钮切换)是只能兼容L+的Vector动画。 每个Vector动画,基本都包含四部分内容,即: Vector:图像资源 Animated-vector:动画、图像粘合剂 ObjectAnimator:动画资源 代码:启动动画 每个Vector动画通过这四个部分去进行分析,就非常清晰了。 这里展示下Demo的效果图: Vector性能问题 有读者在文章后面留言,询问VectorDrawable的性能问题,这里解释一下。 Bitmap的绘制效率并不一定会比Vector高,它们有一定的平衡点,当Vector比较简单时,其效率是一定比Bitmap高的,所以,为了保证Vector的高效率,Vector需要更加简单,PathData更加标准、精简,当Vector图像变得非常复杂时,就需要使用Bitmap来代替了 Vector适用于ICON、Button、ImageView的图标等小的ICON,或者是需要的动画效果,由于Bitmap在GPU中有缓存功能,而Vector并没有,所以Vector图像不能做频繁的重绘 Vector图像过于复杂时,不仅仅要注意绘制效率,初始化效率也是需要考虑的重要因素 SVG加载速度会快于PNG,但渲染速度会慢于PNG,毕竟PNG有硬件加速,但平均下来,加载速度的提升弥补了绘制的速度缺陷。 Google的这个视频中,已经对Vector的效率问题做了解释,可以参考下: https://www.youtube.com/watch?v=wlFVIIstKmA&feature=youtu.be&t=6m3s 参考 https://medium.com/@shemag8/animated-vector-drawable-e4d7743d372c#.3vkt12j20 https://github.com/jpuderer/AnimatedButton
Android Studio集成Bug管理系统 在Android开发中,对于Bug的管理、追踪是非常重要的,通常,开发和Bug追踪是分开的,提交代码后,需要打开网页来进行Bug管理。 但是!!!你不觉得很麻烦吗,在Android Studio中,你可以进行版本管理,那么为什么就不能进行Bug管理呢?确实,你说的对,完全是可以的!!! 配置Bug管理服务器 选择Tools菜单中的Tasks & Contexts,再选择Configure Servers,如图所示: 弹出菜单如下所示: 点击+号,选择Add Server,如图所示: 这里大家可以选择各种Bug管理工具,几乎包括了市面上常用的各种Bug跟踪管理工具。 由于鄙司使用的是JIRA,所以这里点击JIRA,填入公司JIRA服务器的地址,如图所示: 填入Server、Username和密码即可,点击Test,弹出Success即可。 修改Task Name 默认名为Default Task,大家可以修改Task名,例如这里修改为JIRA: 管理Bug 设置成功后,在菜单栏就会多处一个下拉框,如图所示: 点击Open Task,就会弹出跟你相关的所有JIRA信息,如图所示: 你可以搜索,或者直接点击Bug进去修改相关状态,同时,还能设置相应的commit信息,如图所示: 提交的信息可以根据模板来进行生成,如图所示: 是不是很赞,现在使用Android Studio可以完全替代终端、Git、Bug管理工具,完全成为了一个all in one的集成开发环境了!!!
ddmlib使用入门 ddmlib是DDMS工具的核心,堪称Android SDK中最不为人知的隐藏Boss,它封装了一系列对ADB的功能封装。 DDMS工具虽然已经非常强大,可以展示非常多的Android性能监测数据,但是,它有一个很大的缺点,就是很多数据不能导出,而且很多功能也不能达到自定义的需求,因此,基于这些问题,利用ddmlib来完成自定义的功能定制,就是非常有用的了。 完成DDMS功能的自定义设置,就需要使用到ddmlib这个jar,同时,为了了解DDMS是如何实现这些功能的,还需要引人DDMS的一些库,来了解其指令的实现原理,如图所示: 分别是ddmlib.jar、ddms.jar和ddmuilib.jar,其中ddmlib.jar是核心功能,其它两个是为了查看其实现原理而引人的。 搭建研究环境 在IDEA中创建一个Java项目,并导入这些jar包: . ├── lib │ ├── ddmlib.jar │ ├── ddms.jar │ ├── ddmuilib.jar │ └── guava-18.0.jar 可以看见这里多了一个guava的jar包,该jar是Google的一些拓展库,在导入这些jar包的时候需要进行依赖。这些jar全部引人后,研究DDMS的环境就搭建好了。点击每一个jar,就可以查看其相关的方法和代码了,如图所示: 利用ddmlib连接设备 要使用ddmlib,首先需要连接设备,这是学习、研究ddmlib.jar的第一步,代码如下所示: import com.android.ddmlib.*; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; public class Main { public static void main(String[] args) { IDevice device; AndroidDebugBridge.init(false); AndroidDebugBridge bridge = AndroidDebugBridge.createBridge( "/Users/xuyisheng/Library/Android/sdk/platform-tools/adb", false); waitForDevice(bridge); IDevice devices[] = bridge.getDevices(); device = devices[0]; } private static void waitForDevice(AndroidDebugBridge bridge) { int count = 0; while (!bridge.hasInitialDeviceList()) { try { Thread.sleep(100); count++; } catch (InterruptedException ignored) { } if (count > 300) { System.err.print("Time out"); break; } } } } 这里的代码中使用循环来进行处理的原因是,ADB需要时间来进行设备连接,所以需要等待一段时间来进行连接,一旦设备连接成功,就可以通过IDevice类来进行设备操作了。 ddmlib api使用示例 ddmlib提供了很多API,但是其文档很少,很多东西只能从源码中找,这里举一个例子,利用ddmlib来进行设备截图,代码如下所示: private static void takeScreenshot(IDevice device) { try { RawImage rawScreen = device.getScreenshot(); if (rawScreen != null) { int width = rawScreen.width; int height = rawScreen.height; BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); int index = 0; int indexInc = rawScreen.bpp >> 3; for (int y = 0; y < rawScreen.height; y++) { for (int x = 0; x < rawScreen.width; x++, index += indexInc) { int value = rawScreen.getARGB(index); image.setRGB(x, y, value); } } ImageIO.write(image, "PNG", new File("/Users/xuyisheng/Downloads/temp/test.png")); } } catch (TimeoutException | AdbCommandRejectedException | IOException e) { e.printStackTrace(); } } 利用IDevice的API就可以完成设备的截图操作。 DDMS功能自定义 要使用ddmlib来实现DDMS的功能自定义,就需要先了解DDMS是如何获取这些数据的,例如,我们需要了解DDMS是如何统计cpuinfo、meminfo和gfxinfo,也就是下面这个界面: 假如我们要做App的性能监测,那么这里的CPU、Memory、Frame信息是非常好的,但是DDMS却不能导出数据,所以我们需要进行自定义,那么这个功能,DDMS是如何实现的呢?打开ddmsuilib.jar,如图所示: 找到其中的SysinfoPanel类,从命名就基本可以确定,这个就是我们在DDMS中看见的那个界面,进入代码就更可以确定了,如图所示: 在这里,就可以找到相应的实现原理了,原来就是dumpsys cpuinfo”, “cat /proc/meminfo ; procrank”, “dumpsys gfxinfo而已。OK,掌握了这个方法,再查看其它的功能,就非常简单了。 开源项目 Github上对ddmlib研究的人并不多,可想而知,这个隐藏Boss藏的有多深,目前所知的比较出名的是下面这个项目: https://github.com/cosysoft/device 但这个项目是运行不起来的,因为它引用了一些携程内部的服务器地址,需要做修改才能运行,但它的原理还是不错的,对ddmlib的研究也挺深入的。
aar是Android Studio提供的一个依赖库系统,可以很方便的让主项目来使用库项目的代码、资源。 但如何来给一个aar库传递编译参数呢(传递代码配置是很方便的,通过接口即可,但编译参数是不行的)?这个场景还是非常常见的,例如下面的这样一个项目: ├── app │ ├── build.gradle │ ├── libs │ └── src ├── build.gradle ├── gradle.properties └── testlibrary ├── build.gradle ├── libs └── src 这个示例来自公司对推送SDK的封装,我们都知道,第三方的推送SDK需要配置很多AppKey,这些都是在编译时就需要指定的,鄙司对第三方的推送SDK又做了一层封装,抽出了一个aar库,因此,需要在编译时将AppKey传递给aar。 爆栈上实际上已经有这个提问了,但很遗憾没有人回答,http://stackoverflow.com/questions/32955764/how-to-keep-placeholders-in-an-aars-manifest/32955888 app是我们的主项目,依赖testlibrary这样一个aar库项目(上面的目录中是以源码依赖的,但实际上我们是以aar的方式依赖)。这时候主项目依赖testlibrary的时候,需要给testlibrary传一个key,那么考虑将key写在gradle.properties中,通过manifestPlaceholders来进行引用,也就是这样: testlibrary AndroidMainifest.xml: <meta-data android:name="APP_KEY" android:value="${APP_KEY}"/> testlibrary build.gradle: manifestPlaceholders = [ "APP_KEY" : app_key ] 其中app_key就是写在gradle.properties中的参数。 貌似这种方式就可以解决这种问题,但实际上,编译成aar后,你就会发现,在编译aar的时候,你在AndroidMainifest.xml中申明的manifestPlaceholders就已经被替换调了!而且,不管你怎么做,不替换调manifestPlaceholders的值,是肯定编译不过的。那么是不是意味着manifestPlaceholders这条路是行不通的呢? 我们先来仔细分析下问题的原因,我们在编写aar代码的时候,希望aar能够接收外界传来的编译参数,但是,在编译aar的时候,需要提供具体的值来替换这些manifestPlaceholders,否则,则编译不过,貌似整个过程就陷入了一个死循环。。。 解决办法自然是有的,比如,使用一个特殊的标志符,例如xxxxx_abc这样的标志,在主项目中,通过Task来进行Mainifest的替换,但是,这肯定不是我们想要的,因为,Gradle没有这么Low啊!!!解决的方法就是对Gradle文档进行阅读理解!!!地址如下: http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger#TOC-Placeholder-support 我们定位到Android Manifest file merging,好好理解其中的每一句话,只到我们读到这句话: The syntax for placeholder values is ${name} since @ is reserved for links. After the last file merging occurred, and before the resulting merged android manifest file is written out, all values with a placeholder will be swapped with injected values. A build breakage will be generated if a variable name is unknown. 有点意思吧,除了我们常用的${}的manifestPlaceholders写法,实际上,还有一种以@开头的写法!!! OK,这种写法的含义就是,通过@开头来指定manifestPlaceholders的Key的时候,表示当前编译不执行manifestPlaceholders的替换!!!那么通过这种方式,我们就可以生成带manifestPlaceholders的aar库,从而解决我们前面提到的这个问题。 具体的解决方式如下: testlibrary AndroidMainifest.xml: <meta-data android:name="APP_KEY" android:value="${APP_KEY}"/> testlibrary build.gradle: manifestPlaceholders = [ "@APP_KEY" : "" ] 是的,你没有看错,前面加一个@就可以了,这样你在编译aar的时候,就会保留本库中的manifestPlaceholders而不做任何替换!!!通过这样的设置,你就可以在主项目引用的时候再进行manifestPlaceholders的替换,从而实现编译参数传递。 在主项目中,配置manifestPlaceholders即可。 app build.gradle: manifestPlaceholders = [ "APP_KEY" : app_key ] 这里的app_key就是写在gradle.properties中的参数。 So easy,一个字符解决了所有问题。
Gradle自定义插件 在Gradle中创建自定义插件,Gradle提供了三种方式: 在build.gradle脚本中直接使用 在buildSrc中使用 在独立Module中使用 开发Gradle插件可以在IDEA中进行开发,也可以在Android Studio中进行开发,它们唯一的不同,就是IDEA提供了Gradle开发的插件,比较方便创建文件和目录,而Android Studio中,开发者需要手动创建(但实际上,这些目录并不多,也不复杂,完全可以手动创建)。 在项目中使用 在Android Studio中创建一个标准的Android项目,整个目录结构如下所示: ├── app │ ├── build.gradle │ ├── libs │ └── src │ ├── androidTest │ │ └── java │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── res │ └── test ├── build.gradle ├── buildSrc │ ├── build.gradle ---1 │ └── src │ └── main │ ├── groovy ---2 │ └── resources ---3 ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── local.properties └── settings.gradle 其中,除了buildSrc目录以外,都是标准的Android目录,而buildSrc就是Gradle提供的在项目中配置自定义插件的默认目录,开发Gradle要创建的目录,也就是RootProject/src/main/groovy和RootProject/src/main/resources两个目录。 在配置完成后,如果配置正确,对应的文件夹将被IDE所识别,成为对应类别的文件夹。 创建buildSrc/build.gradle—1 首先,先来配置buildSrc目录下的build.gradle文件,这个配置比较固定,脚本如下所示: apply plugin: 'groovy' dependencies { compile gradleApi() compile localGroovy() } 创建Groovy脚本—2 接下来,在groovy目录下,创建一个Groovy类(与Java类似,可以带包名,但Groovy类以.grovvy结尾),如图所示: 在脚本中通过实现gradle的Plugin接口,实现apply方法即可,脚本如下所示: package com.xys import org.gradle.api.Plugin import org.gradle.api.Project public class MainPluginForBuildSrc implements Plugin<Project> { @Override void apply(Project project) { project.task('testPlugin') << { println "Hello gradle plugin in src" } } } 在如上所示的脚本的apply方法中,笔者简单的实现了一个task,命名为testPlugin,执行该Task,会输出一行日志。 创建Groovy脚本的Extension 所谓Groovy脚本的Extension,实际上就是类似于Gradle的配置信息,在主项目使用自定义的Gradle插件时,可以在主项目的build.gradle脚本中通过Extension来传递一些配置、参数。 创建一个Extension,只需要创建一个Groovy类即可,如图所示: 如上所示,笔者命名了一个叫MyExtension的groovy类,其脚本如下所示: package com.xys; class MyExtension { String message } MyExtension代码非常简单,就是定义了要配置的参数变量,后面笔者将具体演示如何使用。 在Groovy脚本中使用Extension 在创建了Extension之后,需要修改下之前创建的Groovy类来加载Extension,修改后的脚本如下所示: package com.xys import org.gradle.api.Plugin import org.gradle.api.Project public class MainPluginForBuildSrc implements Plugin<Project> { @Override void apply(Project project) { project.extensions.create('pluginsrc', MyExtension) project.task('testPlugin') << { println project.pluginsrc.message } } } 通过project.extensions.create方法,来将一个Extension配置给Gradle即可。 创建resources—3 resources目录是标识整个插件的目录,其目录下的结构如下所示: └── resources └── META-INF └── gradle-plugins 该目录结构与buildSrc一样,是Gradle插件的默认目录,不能有任何修改。创建好这些目录后,在gradle-plugins目录下创建——插件名.properties文件,如图所示: 如上所示,这里笔者命名为pluginsrc.properties,在该文件中,代码如下所示: implementation-class=com.xys.MainPluginForBuildSrc 通过上面的代码指定最开始创建的Groovy类即可。 在主项目中使用插件 在主项目的build.gradle文件中,通过apply指令来加载自定义的插件,脚本如下所示: apply plugin: 'pluginsrc' 其中plugin的名字,就是前面创建pluginsrc.properties中的名字——pluginsrc,通过这种方式,就加载了自定义的插件。 配置Extension 在主项目的build.gradle文件中,通过如下所示的代码来加载Extension: pluginsrc{ message = 'hello gradle plugin' } 同样,领域名为插件名,配置的参数就是在Extension中定义的参数名。 配置完毕后,就可以在主项目中使用自定义的插件了,在终端执行gradle testPlugin指令,结果如下所示: :app:testPlugin hello gradle plugin 在本地Repo中使用 在buildSrc中创建自定义Gradle插件只能在当前项目中使用,因此,对于具有普遍性的插件来说,通常是建立一个独立的Module来创建自定义Gradle插件。 创建Android Library Module 首先,在主项目的工程中,创建一个普通的Android Library Module,并删除其默认创建的目录,修改为Gradle插件所需要的目录,即在buildSrc目录中的所有目录,如图所示: 如上图所示,创建的文件与在buildSrc目录中创建的文件都是一模一样的,只是这里在一个自定义的Module中创建插件而不是在默认的buildSrc目录中创建。 部署到本地Repo 因为是通过自定义Module来创建插件的,因此,不能让Gradle来自动完成插件的加载,需要手动进行部署,所以,需要在插件的build.gradle脚本中增加Maven的配置,脚本如下所示: apply plugin: 'groovy' apply plugin: 'maven' dependencies { compile gradleApi() compile localGroovy() } repositories { mavenCentral() } group='com.xys.plugin' version='2.0.0' uploadArchives { repositories { mavenDeployer { repository(url: uri('../repo')) } } } 相比buildSrc中的build.gradle脚本,这里增加了Maven的支持和uploadArchives这样一个Task,这个Task的作用就是将该Module部署到本地的repo目录下。在终端中执行gradle uploadArchives指令,将插件部署到repo目录下,如图所示: 当插件部署到本地后,就可以在主项目中引用插件了。 当插件正式发布后,可以把插件像其它module一样发布到中央库,这样就可以像使用中央库的库项目一样来使用插件了。 引用插件 在buildSrc中,系统自动帮开发者自定义的插件提供了引用支持,但自定义Module的插件中,开发者就需要自己来添加自定义插件的引用支持。在主项目的build.gradle文件中,添加如下所示的脚本: apply plugin: 'com.xys.plugin' buildscript { repositories { maven { url uri('../repo') } } dependencies { classpath 'com.xys.plugin:plugin:2.0.0' } } 其中,classpath指定的路径,就是类似compile引用的方式,即——插件名:group:version 配置完毕后,就可以在主项目中使用自定义的插件了,在终端执行gradle testPlugin指令,结果如下所示: :app:testPlugin Hello gradle plugin 如果不使用本地Maven Repo来部署,也可以拿到生成的插件jar文件,复制到libs目录下,通过如下所示的代码来引用: classpath fileTree(dir: 'libs', include: '\*.jar') // 使用jar 参考:https://docs.gradle.org/current/userguide/custom_plugins.html
从Bitmap.recycle说起 在Android中,Bitmap的存储分为两部分,一部分是Bitmap的数据,一部分是Bitmap的引用。 在Android2.3时代,Bitmap的引用是放在堆中的,而Bitmap的数据部分是放在栈中的,需要用户调用recycle方法手动进行内存回收,而在Android2.3之后,整个Bitmap,包括数据和引用,都放在了堆中,这样,整个Bitmap的回收就全部交给GC了,这个recycle方法就再也不需要使用了。 然而…… 现在的SDK中对recycle方法是这样注释的,如图所示: 可以发现,系统建议你不要手动去调用,而是让GC来进行处理不再使用的Bitmap。我们可以认为,即使在Android2.3之后的版本中去调用recycle,系统也是会强制回收内存的,只是系统不建议这样做而已。 鄙司代码有些是从Android2.3出来的,因此很多地方还在使用Bitmap.recycle。通常情况下,这也没什么问题,但是,今天遇到一个bug引发了Bitmap.recycle的血案。 起因 这个bug的起因是因为我们的一张图片需要旋转,同时可以设置一个旋转角度,老的代码是这样写的: ImageView imageView = (ImageView) findViewById(R.id.test); Matrix matrix = new Matrix(); matrix.setRotate(0.013558723994643297f); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); Bitmap targetBmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (!bitmap.isRecycled()) { bitmap.recycle(); } imageView.setImageBitmap(targetBmp); 除了中间的0.013558723994643297f这串比较奇葩的数据(当然,正常情况下都是20、30这样正常的数),其它都是比较正常的代码。 但实际上,只要一运行这段代码,程序就会崩溃,错误原因如下所示: E/AndroidRuntime: FATAL EXCEPTION: main Process: com.xys.preferencetest, PID: 30512 java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@1a50ff6b 这个问题一看就知道是由于Bitmap被调用recycle方法回收后,又调用了Bitmap的一些方法而导致的。可是,代码中可以发现我们recycle的是bitmap而不是通过Bitmap.createBitmap重新生成的targetBmp,为什么会报这个exception呢? 注释 按道理来说,bitmap与create出来的targetBmp应该是两个对象,当旋转角度正常的时候,确实也是这样,但当旋转角度比较奇葩的时候,这两个bitmap对象居然变成了同一个!而打开Bitmap.createBitmap的代码,可以发现如下所示的注释: 这里居然写着:The new bitmap may be the same object as source, or a copy may have been made. 看来还是真有可能为同一个对象的! 猜测 经过几次尝试,发现只有在角度很小很小的时候,才会出现这个情况,两个bitmap是同一个对象,因此,我只能这样猜测,当角度过小时,系统认为这是一张图片,没有发生变化,那么系统就直接引用同一个对象来进行操作,避免内存浪费。那么这个角度是怎么来的呢?继续猜测,如图所示: 当图像的旋转角度小余两个像素点之间的夹角时,图像即使选择也无法显示,因此,系统完全可以认为图像没有发生变化,因此,注释中的情况,是不是有可能就是说的这种情况呢? 我还没有来得及继续验证,希望大家可以一起讨论下~有说的不对的还请指教。 然而…… 然而,教训是,在不兼容Android2.3的情况下,别在使用recycle方法来管理Bitmap了,那是GC的事!
AS2.0大步更新 Google强势逆天 就在不久前,Google高调发布了Android Studio 2.0,是的,他19号才发布了Android Studio 1.5,才过了一个礼拜,很多人都是昨天才更新了1.5,一看今天就2.0了,步子跨的太大,不会疼嘛。不过没事,程序员还怕死嘛,马上更新。 New Features in Android Studio 2.0 Instant Run: Faster Build & Deploy 逆天吗?你还在羡慕iOS的playground吗?Android现在有了自己的原生LayoutCast插件。第一次运行后,就可以快速在真机中看见修改后的效果。最关键的是,不光UI可以,代码逻辑同样可以!当年乔布斯减少了10秒Mac的启动时间,就节省了几亿人的时间,现在AS instant run是把开发者的生命又延长了一个数量级啊! GPU Profiler AS2.0新增了OpenGL ES的debug工具,可以对GPU进行逐帧分析。对游戏开发者应该是非常大的福利。 Gradle Grade速度真的快了、快了、快了。 The hard days of Android developers has gone… the hard days..演讲者也真是够了,原来你们也知道以前真的很慢、很慢、很慢啊。 既然你现在快了,那么我就原谅你了。 新的模拟器 好像不需要Genymotion了……原生的模拟器速度越来越快了,还支持Arm、x86,多人性化。 More 从下面的网站,你可以了解关于Android Studio 2.0的一切: release note https://sites.google.com/a/android.com/tools/tech-docs/instant-run Development Blog http://android-developers.blogspot.jp/2015/11/android-studio-20-preview.html 直播视频 https://androiddevsummit.withgoogle.com/
Android快捷方式解密 Android快捷方式作为Android设备的杀手锏技能,一直都是非常重要的一个功能,也正是如此,各种流氓App也不断通过快捷方式霸占着这样一个用户入口。 同时,各大国产ROM和Luncher的崛起,让这个桌面之争变的更加激烈。毕竟大家都只想用户用自己的App资源,所以,现在各大App不仅仅是要抢占入口,同时还要和各大ROM斗智斗勇。本文将对这个快捷方式进行深度解密,同时给出App适配各种ROM的整合方案。 本文很多地方参考了这位朋友的实现: https://gist.github.com/waylife/437a3d98a84f245b9582 特此表示感谢! 创建快捷方式之——少林派 所谓少林,是指系统正统的解决方法 天下武功出少林,天下的快捷方式都是Google给的,我们先来看看如何使用Android系统提供的方式来使用Android的快捷方式。 首先大家要知道各种Launcher的区别,原生的Launcher,是两层结构,桌面是快捷方式,而进去后的App列表是App的Launch Icon;而以小米为首的一帮ROM,参考iOS风格,将Launcher改为了一层,即直接显示Launch Icon。 权限设置 <!-- 添加快捷方式 --> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <!-- 移除快捷方式 --> <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" /> <!-- 查询快捷方式 --> <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" /> 创建快捷方式 创建快捷方式的Action: // Action 添加Shortcut public static final String ACTION_ADD_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; 通过广播创建快捷方式: /** * 添加快捷方式 * * @param context context * @param actionIntent 要启动的Intent * @param name name */ public static void addShortcut(Context context, Intent actionIntent, String name, boolean allowRepeat, Bitmap iconBitmap) { Intent addShortcutIntent = new Intent(ACTION_ADD_SHORTCUT); // 是否允许重复创建 addShortcutIntent.putExtra("duplicate", allowRepeat); // 快捷方式的标题 addShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); // 快捷方式的图标 addShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, iconBitmap); // 快捷方式的动作 addShortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent); context.sendBroadcast(addShortcutIntent); } 参数相信大家都能看得懂,只是有一点需要注意的,duplicate这个属性,是设置该快捷方式是否允许多次创建的属性,但是,在很多ROM上都不能成功识别,嗯,这就是我们最开始说的快捷方式乱现象。 删除快捷方式 删除快捷方式的Action: // Action 移除Shortcut public static final String ACTION_REMOVE_SHORTCUT = "com.android.launcher.action.UNINSTALL_SHORTCUT"; 通过广播删除快捷方式: /** * 移除快捷方式 * * @param context context * @param actionIntent 要启动的Intent * @param name name */ public static void removeShortcut(Context context, Intent actionIntent, String name) { Intent intent = new Intent(ACTION_REMOVE_SHORTCUT); intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); // intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.putExtra("duplicate", false); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent); context.sendBroadcast(intent); } 参数与创建快捷方式的方法击败类似,需要注意的是,Intent.EXTRA_SHORTCUT_INTENT,与之前创建快捷方式的Intent必须要是同一个,不然是无法删除快捷方式的。 创建快捷方式之——逍遥派 所谓逍遥派,是指我们从原理来理解如何来适配各种Launcher。 原生的快捷方式添加方法,虽然是官方提供的,但在天国这样一个怎么说呢的国家里,基本是很难使用、适配的,也就是我们最开始说的那些原因。下面我们先从快捷方式的整个生命周期来了解下产生、添加、删除快捷方式的原理,再来思考如何实现多ROM、Launcher的适配。 快捷方式的存储 快捷方式其实都存储在Launcher的数据库中,我们在手机上打开SQLite Editor打开Launcher的数据库。 我们打开Launcher.db的favorite表,这里就是我们保存的快捷方式数据: 几个主要的字段大家基本一看就懂:title、intent、iconResource、icon,分别对应快捷方式名称,快捷方式intent,快捷方式图标来源,快捷方式图标二进制数据。 快捷方式的创建 了解了快捷方式的存储原理,我们就可以针对这个数据库来做文章,所有的快捷方式都可以通过修改这个数据库来实现,同时还不用太考虑兼容性问题。 对于快捷方式的创建,我们依然可以使用系统提供的方法,所以这里不再多说。 快捷方式的判断是否存在 前面我们说了,通过duplicate属性可以区分是否允许创建重复的快捷方式,但是,很多ROM是无法兼容到的,所以,这里我们使用查询Launcher数据库的方式来实现。 我们先来看代码: /** * 检查快捷方式是否存在 <br/> * <font color=red>注意:</font> 有些手机无法判断是否已经创建过快捷方式<br/> * 因此,在创建快捷方式时,请添加<br/> * shortcutIntent.putExtra("duplicate", false);// 不允许重复创建<br/> * 最好使用{@link #isShortCutExist(Context, String, Intent)} * 进行判断,因为可能有些应用生成的快捷方式名称是一样的的<br/> */ public static boolean isShortCutExist(Context context, String title) { boolean result = false; try { ContentResolver cr = context.getContentResolver(); Uri uri = getUriFromLauncher(context); Cursor c = cr.query(uri, new String[]{"title"}, "title=? ", new String[]{title}, null); if (c != null && c.getCount() > 0) { result = true; } if (c != null && !c.isClosed()) { c.close(); } } catch (Exception e) { result = false; e.printStackTrace(); } return result; } /** * 不一定所有的手机都有效,因为国内大部分手机的桌面不是系统原生的<br/> * 更多请参考{@link #isShortCutExist(Context, String)}<br/> * 桌面有两种,系统桌面(ROM自带)与第三方桌面,一般只考虑系统自带<br/> * 第三方桌面如果没有实现系统响应的方法是无法判断的,比如GO桌面<br/> */ public static boolean isShortCutExist(Context context, String title, Intent intent) { boolean result = false; try { ContentResolver cr = context.getContentResolver(); Uri uri = getUriFromLauncher(context); Cursor c = cr.query(uri, new String[]{"title", "intent"}, "title=? and intent=?", new String[]{title, intent.toUri(0)}, null); if (c != null && c.getCount() > 0) { result = true; } if (c != null && !c.isClosed()) { c.close(); } } catch (Exception ex) { result = false; ex.printStackTrace(); } return result; } private static Uri getUriFromLauncher(Context context) { StringBuilder uriStr = new StringBuilder(); String authority = LauncherUtil.getAuthorityFromPermissionDefault(context); if (authority == null || authority.trim().equals("")) { authority = LauncherUtil.getAuthorityFromPermission(context, LauncherUtil.getCurrentLauncherPackageName(context) + ".permission.READ_SETTINGS"); } uriStr.append("content://"); if (TextUtils.isEmpty(authority)) { int sdkInt = android.os.Build.VERSION.SDK_INT; if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的 uriStr.append("com.android.launcher.settings"); } else if (sdkInt < 19) {// Android 4.4以下 uriStr.append("com.android.launcher2.settings"); } else {// 4.4以及以上 uriStr.append("com.android.launcher3.settings"); } } else { uriStr.append(authority); } uriStr.append("/favorites?notify=true"); return Uri.parse(uriStr.toString()); } 这里有两个重载的isShortCutExist方法,唯一的区别就是最后一个参数——intent,加这个参数的原因,在注释中已经写了,更加精确。而getUriFromLauncher方法,是给调用的ContentResolver提供Uri。构造的时候,可以看见,Android的版本话碎片问题,是多么的严重…… 这样在添加快捷方式前,通过这个判断下,就可以只添加一个快捷方式了。 为任意PackageName的App添加快捷方式 知道了我们是如何判断快捷方式是是否存在的,我们就可以通过这种思路来为任意PackageName的App添加快捷方式,代码如下: /** * 为PackageName的App添加快捷方式 * * @param context context * @param pkg 待添加快捷方式的应用包名 * @return 返回true为正常执行完毕 */ public static boolean addShortcutByPackageName(Context context, String pkg) { // 快捷方式名 String title = "unknown"; // MainActivity完整名 String mainAct = null; // 应用图标标识 int iconIdentifier = 0; // 根据包名寻找MainActivity PackageManager pkgMag = context.getPackageManager(); Intent queryIntent = new Intent(Intent.ACTION_MAIN, null); queryIntent.addCategory(Intent.CATEGORY_LAUNCHER);// 重要,添加后可以进入直接已经打开的页面 queryIntent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); queryIntent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); List<ResolveInfo> list = pkgMag.queryIntentActivities(queryIntent, PackageManager.GET_ACTIVITIES); for (int i = 0; i < list.size(); i++) { ResolveInfo info = list.get(i); if (info.activityInfo.packageName.equals(pkg)) { title = info.loadLabel(pkgMag).toString(); mainAct = info.activityInfo.name; iconIdentifier = info.activityInfo.applicationInfo.icon; break; } } if (mainAct == null) { // 没有启动类 return false; } Intent shortcut = new Intent( "com.android.launcher.action.INSTALL_SHORTCUT"); // 快捷方式的名称 shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); // 不允许重复创建 shortcut.putExtra("duplicate", false); ComponentName comp = new ComponentName(pkg, mainAct); shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, queryIntent.setComponent(comp)); // 快捷方式的图标 Context pkgContext = null; if (context.getPackageName().equals(pkg)) { pkgContext = context; } else { // 创建第三方应用的上下文环境,为的是能够根据该应用的图标标识符寻找到图标文件。 try { pkgContext = context.createPackageContext(pkg, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } } if (pkgContext != null) { Intent.ShortcutIconResource iconRes = Intent.ShortcutIconResource .fromContext(pkgContext, iconIdentifier); shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconRes); } // 发送广播,让接收者创建快捷方式 // 需权限<uses-permission // android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> context.sendBroadcast(shortcut); return true; } 创建快捷方式之——星宿派 所谓星宿派,是指我们使用一些Trick来解决多Launcher适配的问题。 由于快捷方式的碎片化非常严重,所以,你顾得上这种ROM,顾不上其它ROM。例如,在原生ROM上,你需要使用类似原生的Launcher权限: <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" /> 但是,在其它ROM上呢,例如华为,你需要这样的权限: <uses-permission android:name="com.huawei.launcher3.permission.READ_SETTINGS" /> <uses-permission android:name="com.huawei.launcher3.permission.WRITE_SETTINGS" /> 为了程序能够通用性够强,理论上我们得为所有不使用原生Launcher权限的Launcher配置权限代码,是的,你妹听错,是所有,只有通过这种奇技淫巧,才能适配更多的Launcher,这里贴一部分给大家爽一下: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_SETTINGS"/> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.android.launcher2.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.launcher2.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.android.launcher3.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" /> <uses-permission android:name="org.adw.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="org.adw.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.htc.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.qihoo360.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.qihoo360.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.lge.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.lge.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="net.qihoo.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="net.qihoo.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="org.adwfreak.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="org.adwfreak.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="org.adw.launcher_donut.permission.READ_SETTINGS" /> <uses-permission android:name="org.adw.launcher_donut.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.huawei.launcher3.permission.READ_SETTINGS" /> <uses-permission android:name="com.huawei.launcher3.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.fede.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.fede.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.sec.android.app.twlauncher.settings.READ_SETTINGS" /> <uses-permission android:name="com.sec.android.app.twlauncher.settings.WRITE_SETTINGS" /> <uses-permission android:name="com.anddoes.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.anddoes.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.tencent.qqlauncher.permission.READ_SETTINGS" /> <uses-permission android:name="com.tencent.qqlauncher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.huawei.launcher2.permission.READ_SETTINGS" /> <uses-permission android:name="com.huawei.launcher2.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.android.mylauncher.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.mylauncher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.ebproductions.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.ebproductions.android.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.miui.mihome2.permission.READ_SETTINGS" /> <uses-permission android:name="com.miui.mihome2.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="telecom.mdesk.permission.READ_SETTINGS" /> <uses-permission android:name="telecom.mdesk.permission.WRITE_SETTINGS" /> <uses-permission android:name="dianxin.permission.ACCESS_LAUNCHER_DATA" /> 这时候大家肯定要问了,你申请这么多权限,用户在安装App的时候,不是要崩溃了,尼玛,这么多看都看不过来啊,其实,根本不需要担心,因为这些基本都是各自ROM中的第三方ROM权限,在用户安装的时候,他们通常会被解析成原生Launcher的权限,例如:添加、修改桌面快捷方式。并不会将所有的权限都写出来。 创建快捷方式之——西域派 所谓西域派,是因为我想不出其他名字了。西域一派,使用其他方式来实现类似快捷方式的方法。 快捷方式的确是我们为应用导流的一个非常重要的入口,但是,由于碎片化实在太严重,所以,我们可以使用在Launcher App列表中为应用增加一个入口的方式来为App导流,简单的说,就是增进一个App的入口Activity。 <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.hujiang.hj_shortcut_lib.HJShortcutActivity" android:theme="@style/Base.Theme.AppCompat.Dialog"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> 非常简单,相信大家都知道这种方式来给App增加一个Activity入口。但是,这种方式,我们如何能够自由的控制这个入口是否显示呢? 奇技PackageManager PackageManager提供了一系列Package的管理方法,当然,也包含了我们非常关心的启用、停用组件这一方法,这个方法在Root情况下,可以修改任一App的任意组件,在普通情况下,对自身App有绝对权限。使用方法也非常简单: public static void toggleFlowEntrance(Context context, Class launcherClass) { PackageManager packageManager = context.getPackageManager(); ComponentName componentName = new ComponentName(context, launcherClass); int res = packageManager.getComponentEnabledSetting(componentName); if (res == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT || res == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { // 隐藏应用图标 packageManager.setComponentEnabledSetting( componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } else { // 显示应用图标 packageManager.setComponentEnabledSetting( componentName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, PackageManager.DONT_KILL_APP); } } 一统江湖 前面我们分析了各种快捷方式、Launcher入口的方式来对App进行导流,当然,这不是我们的目的,我们的目的是能够掌握Android快捷方式的哭花宝典而不用那个啥。 所以,下面我封装了一个shortcut的开源库,从而可以尽可能的忽略ROM的差异,来使用快捷方式和Launcher入口。 项目地址: https://github.com/xuyisheng/ShortcutHelper 目前该项目还在测试阶段,还要很多问题和适配bug需要解决,欢迎大家提issue。 README如下: ShortcutLib使用指南 本项目目前还在测试阶段,请大家多提issue,共同完善。 项目意义 快速使用shortcut,避免各种ROM适配导致的各种问题。 项目可用功能API 增加快捷方式 /** * 添加快捷方式 * * @param context context * @param actionIntent 要启动的Intent * @param name name * @param allowRepeat 是否允许重复 * @param iconBitmap 快捷方式图标 */ public static void addShortcut(Context context, Intent actionIntent, String name, boolean allowRepeat, Bitmap iconBitmap) 判断快捷方式是否存在 基础方式 /** * 判断快捷方式是否存在 * <p/> * 检查快捷方式是否存在 <br/> * <font color=red>注意:</font> 有些手机无法判断是否已经创建过快捷方式<br/> * 因此,在创建快捷方式时,请添加<br/> * shortcutIntent.putExtra("duplicate", false);// 不允许重复创建<br/> * 最好使用{@link #isShortCutExist(Context, String, Intent)} * 进行判断,因为可能有些应用生成的快捷方式名称是一样的的<br/> * * @param context context * @param title 快捷方式名 * @return 是否存在 */ public static boolean isShortCutExist(Context context, String title) 严格方式(增加Intent的检查) /** * 判断快捷方式是否存在 * <p/> * 不一定所有的手机都有效,因为国内大部分手机的桌面不是系统原生的<br/> * 更多请参考{@link #isShortCutExist(Context, String)}<br/> * 桌面有两种,系统桌面(ROM自带)与第三方桌面,一般只考虑系统自带<br/> * 第三方桌面如果没有实现系统响应的方法是无法判断的,比如GO桌面<br/> * * @param context context * @param title 快捷方式名 * @param intent 快捷方式Intent * @return 是否存在 */ public static boolean isShortCutExist(Context context, String title, Intent intent) 更新快捷方式 /** * 更新桌面快捷方式图标,不一定所有图标都有效(有可能需要系统权限) * * @param context context * @param title 快捷方式名 * @param intent 快捷方式Intent * @param bitmap 快捷方式Icon */ public static void updateShortcutIcon(Context context, String title, Intent intent, Bitmap bitmap) 需要注意的是,更新快捷方式在很多手机上都不能生效,需要系统权限。可以通过先删除、再新增的方式来实现。 为任意PackageName的App添加快捷方式 /** * 为任意PackageName的App添加快捷方式 * * @param context context * @param pkg 待添加快捷方式的应用包名 * @return 返回true为正常执行完毕 */ public static boolean addShortcutByPackageName(Context context, String pkg) 移除快捷方式 /** * 移除快捷方式 * * @param context context * @param actionIntent 要启动的Intent * @param name name */ public static void removeShortcut(Context context, Intent actionIntent, String name) 显示隐藏Launcher入口 /** * 显示\隐藏Launcher入口 * * @param context context * @param launcherClass launcherClass */ public static void toggleFlowEntrance(Context context, Class launcherClass) 使用Launcher入口需要在AndroidMainifest文件中注册新增的入口Activity,例如: <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.xxx.xxxxx" android:theme="@style/Base.Theme.AppCompat.Dialog"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> 使用示例 public void addShortcutTest(View view) { // 系统方式创建 // ShortcutUtils.addShortcut(this, getShortCutIntent(), mShortcutName); // 创建前判断是否存在 if (!ShortcutSuperUtils.isShortCutExist(this, mShortcutName, getShortCutIntent())) { ShortcutUtils.addShortcut(this, getShortCutIntent(), mShortcutName, false, BitmapFactory.decodeResource(getResources(), com.hujiang.hj_shortcut_lib.R.drawable.ocsplayer)); finish(); } else { Toast.makeText(this, "Shortcut is exist!", Toast.LENGTH_SHORT).show(); } // 为某个包创建快捷方式 // ShortcutSuperUtils.addShortcutByPackageName(this, this.getPackageName()); } public void removeShortcutTest(View view) { ShortcutUtils.removeShortcut(this, getShortCutIntent(), mShortcutName); } public void updateShortcutTest(View view) { ShortcutSuperUtils.updateShortcutIcon(this, mShortcutName, getShortCutIntent(), BitmapFactory.decodeResource(getResources(), com.hujiang.hj_shortcut_lib.R.mipmap.ic_launcher)); } public void toggleFlowEntrance(View view) { FlowEntranceUtil.toggleFlowEntrance(this, HJShortcutActivity.class); } private Intent getShortCutIntent() { // 使用MAIN,可以避免部分手机(比如华为、HTC部分机型)删除应用时无法删除快捷方式的问题 Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setClass(MainActivity.this, HJShortcutActivity.class); return intent; }
强迫症的研究——MediaPlayer播放进度条的优化 如何做一个优美、流畅而且准确的播放进度条,也许很多人觉得很简单,但实际上,这个问题在大部分时间都被忽略了。 计时方式的比较 计时方式——主线程中使用Handler – 这种方式最简单,在主线程中通过handler.postDealyed(……, 1000),并在onHandleMessage中继续post消息,这样就实现了每隔1000ms进行一次消息循环。 计时方式——使用单独计时线程 – 单独创建一个计时线程,每秒发出time tick事件,主线程通过该事件来更新进度。这种方式比较麻烦,但是不麻烦怎么装逼呢? 如何高雅、准确的实现 对于Handler方式 自身误差 这种方式下,如果使用handler.postDealyed(……, 1000)方式来进行每秒的计时,是不准确的,是的,有很大误差,误差的原因在于在你收到消息,到你重新发出handler.postDealyed的时间,并不是瞬间完成的,这里面有很多逻辑处理的时间,即使没有逻辑处理的时间,handler本身也是耗损性能的,所以消息并不可能按照理想的1000延迟来进行发送,这就导致了误差的累积。 线程调度误差 我们知道,当音乐线程启动,到handler发出消息,这一段时间内,存在进程调度或者其它逻辑的耗时操作,导致这两个时间并不是同时发生的。所以,我们每次在post的时候,都需要对计时进行下补偿,但是,怎么做呢? 对于Handler方式的优化 我们知道,Android中有很多计时的控件,首先想到的是DigitalClock,结果发现已经废弃,好吧,看被什么替换了,OK,发现了TextClock,代码多了不少,感觉更牛逼了。我们直接看他是怎么处理这个问题的: 同样是通过程序员的嗅觉找到这里: private final Runnable mTicker = new Runnable() { public void run() { onTimeChanged(); long now = SystemClock.uptimeMillis(); long next = now + (1000 - now % 1000); getHandler().postAtTime(mTicker, next); } }; 哎呦,有点意思,我们之前是通过postDelay来触发消息事件的,但这里系统使用了postAtTime,这是为什么呢?很自然我们会想到前面两行代码,其实也不用想太多,你代个值进去试下就知道了,假如now取出来是1200,那么next = 1200 + (1000 - 1200 % 1000)也就是next= 2000。你看,虽然我们前一次本该在1000触发的事件,被各种逻辑延迟到1200,那么如果你用postDelay,这个延迟就被累积了,但如果用这种方式,误差就被补偿了。 我们就叫他误差补偿算法吧~ 对于单独计时线程方式 对于单独计时的线程,由于时间点的触发事件和主线程已经分开了,计时线程就不会受主线程逻辑的阻塞了,所以,只要保证开始时对起始时间差进行下同步就OK了。 对于单独计时线程方式的优化 其实对于单独计时线程来说,已经没有什么好优化的了,而且优点还能再列举不少: 计时逻辑与UI逻辑分离,方便拓展 计时准确,可以将计时线程封装,暴露接口,方便拓展 解耦、装逼 如果你还要再进一步优化的话,可以在计时的时候,使用时间差的方式来统计,虽然没什么乱用。
ViewPager不为人知的秘密 ViewPager翻页控制 关于控制ViewPager的翻页,在网上已经有很多解决方法了,我们一个个来看看。 setScanScroll() 我们先来看一下具体实现: public class CustomViewPager extends ViewPager { private boolean isCanScroll = true; public CustomViewPager(Context context) { super(context); } public CustomViewPager(Context context, AttributeSet attrs) { super(context, attrs); } public void setScanScroll(boolean isCanScroll){ this.isCanScroll = isCanScroll; } @Override public void scrollTo(int x, int y){ if (isCanScroll){ super.scrollTo(x, y); } } } 通过控制isCanScroll变量,设置给scrollTo()方法,控制是否能滑动,看上去非常完美,实际上是最不靠谱的方法,因为你setScanScroll()调用之后状态就无法再修改这个状态了,甚至是setCurrentItem方法都不能调用了。 修改Touch事件 同样,我们先来看看代码: public class NoScrollViewPager extends ViewPager { public NoScrollViewPager(Context context) { super(context); } public NoScrollViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent arg0) { return false; } @Override public boolean onInterceptTouchEvent(MotionEvent arg0) { return false; } } 这代码也很简单,就是控制ViewPager的Touch事件,这个基本是万能的,毕竟是从根源上入手的。你可以在onTouchEvent和onInterceptTouchEvent中做逻辑的判断。 重写ViewPager 前面两种方法固然可以在一定程度上完成我们的要求,但是显得略2.所以,我们来看这种方式。 首先我们要了解下ViewPager切页的原理,经过一段时间的查找,我们找到了这个类: private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { int targetPage; if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { targetPage = velocity > 0 ? currentPage : currentPage + 1; } else { final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; targetPage = (int) (currentPage + pageOffset + truncator); } if (mItems.size() > 0) { final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); // Only let the user target pages we have items for targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); } return targetPage; } 不用问我是怎么找到的,这是程序员的嗅觉。 这个方法会在切页的时候重定向Page,那么我们只要在这个方法内重新定向到我们想要的Page就好了。 这是ViewPager的控制切页逻辑。 下面我们继续看,其实在ViewPager中,就给我们提供了一个重写的方法——canScroll,看名字就知道了,这个方法是来控制是否能够滑动的,我们来试下,我们先extends ViewPager,然后重写这个方法: @Override protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { boolean result = super.canScroll(v, checkV, dx, x, y); if (dx < 0 && (/*其它控制逻辑**/)) { return true; } return result; } 通过控制这个方法返回值,就可以真真实实的控制ViewPager的滑动了,你可以试一下,当然,肯定是可以的。 那是不是这样就可以了呢?当然不是的,不然我怎么能继续装逼呢? 虽然在大部分时间,这个回调已经可以实现ViewPager的翻页控制了,但是,如果你翻页速度很快,你就会发现,其实这个回调方法的执行,是跟不上你的速度的。如果你翻页很快,是可以跳过去的,如果你打log,你会发现,canScroll虽然会一直回调,但是回调并不是实时的,所以会出现bug。这也是为什么我开始要解释ViewPager翻页原理的原因,真不是我要装逼,而是为你留下的伏笔。 所以,最终的解决方案就是canScroll + determineTargetPage 首先,我们要重写ViewPager,不用害怕,ViewPager没有任何依赖,你可以把整个ViewPager的源代码全部copy过来,而不需要修改一行代码,除了包名。 然后,我们找到determineTargetPage这个方法,将targetPage修改下: private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { int targetPage; if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { targetPage = velocity > 0 ? currentPage : currentPage + 1; } else { final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; targetPage = (int) (currentPage + pageOffset + truncator); } if (mItems.size() > 0) { final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); // Only let the user target pages we have items for targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); } targetPage = reDetermineTargetPage(targetPage); return targetPage; } targetPage = reDetermineTargetPage(targetPage)这个就是我们加的代码,通过reDetermineTargetPage方法,我们来修改ViewPager的targetPage,是不是很无耻的感觉,正常正常。 所以,我们要增加一个父类方法给我们后面继承的ViewPager重写: public int reDetermineTargetPage(int targetPage) { return targetPage; } 最后,我们在继承的ViewPager中,重写这两个方法: public class MyViewPager extends ViewPager { @Override protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { boolean rt = super.canScroll(v, checkV, dx, x, y); if (dx < 0 && (/*其他逻辑控制**/)) { return true; } return rt; } @Override public int reDetermineTargetPage(int targetPage) { int rtn = targetPage; int currentPage = getCurrentItem(); if (targetPage > currentPage && (/* 其他逻辑控制**/)) { rtn = currentPage; } return rtn; } } 这样我们就非常完美的实现了ViewPager的翻页控制,在慢慢翻页的时候,canScroll就可以帮我们控制了,当快速翻页的时候reDetermineTargetPage给我们做了双保险,即使你翻页过去了,你也会被targetPage给带回来。 ViewPager强制刷新UI ViewPager不能动态刷新UI的原因主要是因为PagerAdapter中调用notifyDataSetChanged是会失效的。 通用解决方法 当ViewPager绘制完Item之后,ViewPager会把child标记为POSITION_UNCHANGED,这样就不会在notifyDataSetChanged后更新这个View了。所以,要解决这个问题,我们只需要在: @Override public int getItemPosition(Object object) { return POSITION_NONE; } 当我们调用PagerAdapter的notifyDataSetChanged方法之后,系统会去Adapter的getItemPosition方法中遍历所有的child,我们在上面的方法中改写了返回值,全部返回为POSITION_NONE,表示child都没有绘制过,这样ViewPager就会去重绘了。 更加优化一点的代码如下: @Override public void notifyDataSetChanged() { mChildCount = getCount(); super.notifyDataSetChanged(); } @Override public int getItemPosition(Object object) { // 重写getItemPosition,保证每次获取时都强制重绘UI if (mChildCount > 0) { mChildCount--; return POSITION_NONE; } return super.getItemPosition(object); } 我们增加一个mChildCount来记录子类的数量,在一定程度上减少重绘的次数。 因为重绘的时候,ViewPager会的Destory Item,增加了系统开销。 更加优化的方法 当我们只需要对ViewPager中的某些元素进行更新时,我们可以在instantiateItem方法调用时,用View.setTag方法加入标志,在需要更新View时,通过findViewWithTag的方法找到对应的View进行更新。
把抽奖活动写成一篇技术博客是怎样一种体验 本次活动预备知识贴:天罗地网——Python爬虫初初初探 http://blog.csdn.net/eclipsexys/article/details/48193541 请一定先了解下,不然就真的是为了抽奖了! 抽抽抽抽抽抽奖 我的新书《Android群英传》上市不久,为了回报各位的大力推荐,也希望更多的人能多多支持,特准备此次抽奖活动。 抽奖对象 只要在本博客中留言,即可参与抽奖活动。 留言内容如下: 已购买《Android群英传》的朋友,请在微博中发帖并@Tomcat的猫 (http://weibo.com/1904977584),并在本博客中贴出你的微博地址 或者直接贴出购买链接的地址,并告知我你的用户名 如果你还未购买,那就帮忙转发微博(宣传本书即可,最好配图哈),获得15个以上赞(我肯定会帮你赞的哈~~),并在本博客中贴出你的微博地址 以前在微博、微信中已经宣传过的,只要在评论中@我一下,并在本博客中留言——“已宣传”,并写上你的微信号,就OK了 PS 请不要重复评论,虽然可以增加我的人气,但对抽奖概率,不会有丝毫影响哦。 PS 请不要欺骗医生真挚纯洁真诚善良的心! PS 如果你不要我的书也不要我的补贴,但是你却中奖了,那请你直接来上海,浦软大厦703,我!请!你!吃!食!堂!。 奖品!!! 未购买《Android群英传》的,奖品为签名版《Android群英传》一本(如果觉得我的字太丑,我也支持画押) 已购买《Android群英传》的,奖品为报销你的买书钱 写书不易,一本书我只赚4块钱,请大家本着社会主义的核心价值观,请不要欺骗我真挚的感情~~~~ 奖品数量 (comments / 40) + 1 截止时间 抽奖时间,暂定于2015年9月25日中秋前夕。希望给大家带来一份不错的中秋礼物。 下面是技术帖下面是技术帖下面是技术帖下面是技术帖 如何实现抽奖 抽奖的方式很简单,统计所有的有限留言,获取他们的用户名,通过随机数来确定中奖的人的用户名。 作为一个技术宅,我当然不想自己去统计,能自动化的就不要用女朋友,能写脚本的就不用女朋友。所以,本博客的实际目的在于教大家如何正确的去使用女朋友,哦,不对,是正确的使用脚本。 分析 首先我们来看CSDN博客的评论系统。 哎呀我真不是故意截这么多赞美的,请无视。 我们打开Chrome的审核元素: 用放大镜找到用户名: 然后点击右键去找源代码,可是,我们突然发现,不对呀,源代码中根本就没有这些评论信息啊。 哦,这样应该也对,评论的加载,应该是用ajax的吧,不然我们每次评论后,肯定会刷新整个页面咯。 OK,那么我们就来到Network标签,刷新页面,获取数据: 显示评论所调用的js,就在这些文件当中,我们慢慢找吧。 首先,我们尝试着先过滤几个关键字,比如 comment: 哎哟不错哦,第一个链接看上次嫌疑很大啊,点击右键,新窗口打开: 看来,英语好的人运气都不会太差。 这样我们就非常简单的获取了获得评论的地址: http://blog.csdn.net/eclipsexys/comment/list/47405045?page=1 从URL可以看出来,只是加了个用户名作区分。 OK,下面我们可以通过: 通过Python爬取动态加载的网站 通过Scrapy框架进行爬虫搜索 尼玛,这就是返回了一个Json啊 还爬什么爬,直接请求这个接口,咱们就拿到这些数据了,所以,前面说的本篇的预备帖,好吧,其实是骗流量的。 实现 实现就非常简单了,尼玛,接口都有了,拿了数据,去除重复评论的、无效的评论、回复的评论,剩下的就是有效数据了。 上Python,让看了预备帖的人不至于扫兴而归: # coding:utf-8 import requests import json import random class Prize(object): def __init__(self): print u'开始抽奖啦' # 获取网页信息 def getSource(self, url): head = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'} html = requests.get(url, headers=head) html.encoding = 'utf-8' return html.text # 获取所有评论信息 def getAllCommentInfo(self, source): return json.loads(source)['list'] # 保存到文件 def saveinfo(self, commentInfo): f = open('info.txt', 'w') for each in commentInfo: print each f.writelines('UserName:' + each + '\n') f.close() if __name__ == '__main__': # 设定获奖人数 winnerCount = 1 userList = [] url = "http://blog.csdn.net/eclipsexys/comment/list/47405045?page=1" androidHeros = Prize() html = androidHeros.getSource(url) commentsInfo = androidHeros.getAllCommentInfo(html) for each in commentsInfo: if '[reply]' not in each['Content'] and each['UserName'] not in userList: userList.append(each['UserName']) androidHeros.saveinfo(userList) for i in range(0, winnerCount): randomNum = random.randint(0, userList.__len__()) winner = userList[randomNum] print '\n-------------------Winner : ' + winner + ' -------------------' 多说一句 技术,是为了实现实际的目的,这个世界上没有最好的语言,只有最适合的语言,请用最合适的语言去做最合适的事,拒绝做一个语言喷子,从你我他做起。 ——有感于某群中为了争论爬虫为什么不用Java写的人 切记 切记,是在本博客下留言!!! 抽奖结果 即将到来,请大家奔走相告,开始留言吧,评论的人,运气一般都不会太差。 就在刚刚,新鲜出炉的抽奖结果,请让我大声念出来!!! E:\python\python.exe G:/csdn/csdn.py 开始抽奖啦 jingxia2008 yanyangy_js u013814553 onlyellow jiang89125 S938548157 u011775829 kinglearnjava bc_2014621 u013369232 wenwen091100304 asq1755 dongfeng9ge solidajun caigen0001 coder_nice qq2603825424 a15996088263 u013349626 qmhs815 sbsujjbcy zhuyaozong longwanglidfdfdf u010334329 u010649376 Plcsy2012 F1ReKing wakewakewake u014400934 marktheone oushangfeng123 WX_LYB csdnwangzhan hlglinglong u014626094 nimengbo forzajuve_android freestyle_zmy u013211506 supertian007 www5115zy onlybone baidu_27869435 sinat_16653803 freexiaoyu fewwind AlbertDenver u011326653 cxmscb fmlfch lingling_a gao_chun lianwanfei lw1075219814 Jasonez u012403246 yayun0516 sinat_26871969 meng209292 xiaruoli89 u013364442 fanaidehua jijiaxin1989 xxx823952375 cicf1986 y1scp xiaozhonghuaa wanghao200906 u010026245 WXY9206 u011934921 u014495711 Dakaring darryl0912 wavever Youzh178 muyuhema yeyuxp y505772146 rh1910362960 msdgw beckett1216 u014061684 chen41345507 u010850027 u012138153 daijianweinihao u012994271 elsdnwn aqswde35025 lianlianzhuifeng a06_kassadin DaoFeng905147 feijixiaoyu went0213 github_31318977 m75100313 lijun123456789lijun fhkatuz674 adhere534 h1252680267 Fulgens kainkain1988 u012293381 u014679097 ElinaVampire zhanghongliubob Kamingnnnnng cwc455826074 liu470368500 -------------------Count : 110 ------------------- -------------------Winner : AlbertDenver ------------------- -------------------Winner : y1scp ------------------- -------------------Winner : wanghao200906 ------------------- Process finished with exit code 0 总计有110位网友留言,本来留言是有要求的,但是尼玛都太不按常理出牌了,所以就只是过滤了重复的和回复的,如果你没转发微博、没赞,你TMD还抽中了,我只能说——你赢了!!! 结果如下: 中奖的三个人: wanghao200906: 我感觉会中奖。昨天踩了狗屎 y1scp : 恳请大家尊重原创正版 人人做到自己不触碰不制造盗版、PDF电子版!徐大大,我是来支持你的。 AlbertDenver : 支持医生……http://weibo.com/2104204754/CAN1VliSA 恭喜你们了,特别是那个昨天踩了狗屎的,真的没白踩啊。。。。。 我已经发私信给你们了,要签名版还是购书费,你们说了算,请毫不客气的联系我!!!
《Android群英传》勘误 勘误已经全部更新 《Android群英传》上市以后,收到了很多读者的勘误留言,有很多勘误,我都读了不止一遍才发现问题在哪,可见读者朋友们的细心,在此,我表示最真心的感谢,也对书中这些问题向各位读者表示歉意。 最近,出版社已经准备对《Android群英传》进行第二版的出版,所以,借这个机会,我将书中发现的所有勘误,从头梳理了一遍,并在在第二版中进行了修改,在这里,感谢对本书提出勘误的读者朋友们,非常感谢!!! 我的新书《Android群英传》刚刚上市不久,希望大家多多支持。本篇为本书的勘误,由于时间仓促,书中难免会存在一些错误,特在此列出这些勘误,也希望广大读者发现错误后,及时在本文评论中贴出来,我将收录到下次的修订中,感谢大家的支持和包容~~ 前言-资源与勘误 ……都会上传到Github代码分享平台供大家 frok,下载…… ————-> ……都会上传到Github代码分享平台供大家 fork,下载…… 本书特色 本书各个章节之间并没有严格的 递近 关系,读者可以随时挑选自己感兴趣的章节开始读起。 ————-> 本书各个章节之间并没有严格的 递进 关系,读者可以随时挑选自己感兴趣的章节开始读起。 2.3.3-ADB命令来源P29 多谢 thinkWyp 提出~~ ABD就像一根长长的纽带…… ————-> ADB 就像一根长长的纽带…… 多谢 @thinkWyp 5.1.3-View坐标系P90 getBottom()的图只到ViewGroup上边缘。 多谢 @安卓弟 Android文件目录P12 图1.24和1.25图贴反了。 多谢 @北宅 3.2View的测量P37 图3.5序号重复,分别为图3.5和图3.6 多谢 @北宅 3.6引用UI模板P49 这行代码就是在指定引用的名字控件…… ————-> 这行代码就是在指定引用的名字 空间 …… 多谢@ DIM 3.2View的测量P35 控件大小一般随着控件的子空间…… ————-> 控件大小一般随着控件的子 控件 …… 多谢@程序亦非猿 4.2ListViewP77 toolbarAnim(0) 跟toolbarAnim(1)的注释show以及hide写反了。 多谢@nita510903569 2.1SDKP20 图2.18 SDK Manager的箭头指错了地方,应该是倒数第二个。 多谢@陈启超 6.6旋转变换P138 选择变换即指一个…… ————-> 旋转 变换即指一个…… 错切变换公式:P140 y= k2 x x0 x y0 ————-> y= k2 x x0 +y0 P141 A、B、C、D共同控制 ————-> A、B、D、E共同控制 多谢@nita510903569 12.8Circular RevealP286 最上面一个标题,startRadius。 ————-> 应该是 endRadius。 多谢 @nita510903569 ,堪称火眼金睛! 非常感谢提出的勘误。 9.5Dex2jarP226 一切断电反编译…… ————-> 一切反编译……手抖了。。。。 多谢 @寄莫相伴。 伪勘误 在这篇文章中,由于使用的一些图是早期的一些图,所以可能存在一些错别字,不过正式的书中是没有的。 封底 如果你 整 期待着…… ————-> 如果你 正 期待着…… 目录 第二章序号有问题,请大家无视,正式版是正确的。
环境准备 Python 我们使用Python2.7进行开发,注意配置好环境变量。 IDE 我们使用Pycharm进行开发,它和大名鼎鼎的Android Studio、IDEA同出一门——Jet Brains。 关于破解,很无耻的贴两个: 用户名:yueting3527 注册码: ===== LICENSE BEGIN ===== 93347-12042010 00001FMHemWIs"6wozMZnat3IgXKXJ 2!nV2I6kSO48hgGLa9JNgjQ5oKz1Us FFR8k"nGzJHzjQT6IBG!1fbQZn9!Vi ===== LICENSE END ===== 用户名:yueting3527 注册码: ===== LICENSE BEGIN ===== 93347-12042010 00001FMHemWIs"6wozMZnat3IgXKXJ 2!nV2I6kSO48hgGLa9JNgjQ5oKz1Us FFR8k"nGzJHzjQT6IBG!1fbQZn9!Vi ===== LICENSE END ===== Requests模块 Requests模块是一个用于替代Python URLLib2的一个第三方网络请求库。 安装 Windows:pip install requests Linux & Mac:sudo pip install requests 但由于有些比较奇怪的原因,导致这个下载过程异常艰辛,所以我们经常需要使用这样一个网站来帮助我们下载: http://www.lfd.uci.edu/~gohlke/pythonlibs/ 这里面镜像收集了几乎所有的Python第三方库,我们搜索Requests,点击下载。 下载完毕后,更改后缀名为zip。并将解压出的Requests文件夹放到Python的Lib文件夹下。 通过Requests获取网页源代码 无反爬虫机制 直接使用Requests库的get方法获取网页源代码: import requests html = requests.get('http://www.hujiang.com/') print(html.text) 在终端中,我们就可以看见生成的网页源代码了。 有反爬虫机制 但是,很多网站并不会轻松的让爬虫获取到网页信息,这时候,我们就需要通过修改Http头信息的方式来获取。 例如我们使用同样的代码去爬我的博客 http://blog.csdn.net/eclipsexys 在终端中,我们可以看见这样一段话: <html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>nginx</center> </body> </html> 403,这时候,我们就需要修改下爬虫代码。 首先,我们在页面上点击右键选择审查元素,找到Network,刷新下, 选择任意一个元素,找到最后的User—Agent: User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36 这就是我们的Http请求头。现在我们修改代码: import requests head = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'} html = requests.get('http://blog.csdn.net/eclipsexys', headers = head) print(html.text.encode('utf-8')) 添加请求头,并设置下编码格式为UTF-8。(Windows下默认为GBK,请先修改coding为UTF-8) ps: 在Python文件中,如果我们要输入中文,需要指定下文件的字符集: # coding=utf-8 具体见 https://www.python.org/dev/peps/pep-0263/ 我们再运行,现在就可以正常获取源代码了。 Requests正则搜索 直接get出来的内容,都是网页的所有源代码,这肯定不是我们需要的,所以,我们可以通过正则表达式来提取我们所需要的内容。 例如,我们想提取网页中的所有超链接,OK,我们来看如何实现: re模块 首先我们需要引入re模块,re模块是正则表达式的模块,使用与web端的正则一样: import requests import re head = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'} html = requests.get('http://www.hujiang.com/', headers=head) html.encoding = "utf-8" href = re.findall('<a target="_blank" href="(.*?)"', html.text, re.S) for each in href: print each 向网页提交数据 Get与Post 他们的区别如下所示: - Get是从服务器上获取数据 - Post是向服务器传送数据 - Get通过构造url中的参数来实现功能 - Post将数据放在header提交数据 网页分析工具 Chrome调试功能——Network调试 在Network中找到Post提交的地方,找到Form Data,这就是表单数据。 构造表单 Post方式提交表单。 import requests import re url = 'https://www.crowdfunder.com/browse/deals&template=false' data = { 'entities_only':'true', 'page':'2' } html_post = requests.post(url,data=data) title = re.findall('"card-title">(.*?)</div>',html_post.text,re.S) for each in title: print each XPath XPath,就是XML路径语言,我们在寻找一个元素时,如果使用正则表达式,可以这么说,我要找一个长头发、180cm的女人。那么如果要用XPath来表达,就是XX公司XX部门的前台。 lxml 在Python中使用XPath,我们需要使用第三方模块lxml,安装如同Requests。 获取HTML的XPath路径 打开Chrome的审核元素,我们找到任何一个元素,点击右键,选择copy XPath即可。 当然,我们也可以手写,它的基本语法如下: //定位根节点 /往下层寻找 提取文本内容:/text() 提取属性内容: /@xxxx 比如我们选择这个地址:http://www.imooc.com/course/list?c=android&page=2 打开审核元素: 这样我们就非常方便的获得了元素的XPath,同时,我们也可以根据规则来手动修改。 爬取内容 使用XPath基本是如下三个步骤: from lxml import etree Selector = etree.HTML(HTML Source) Selector.xpath(XPath) 我们同样以前面的网址为例,我们抓取所选的那门课程的标题: # coding=utf-8 import requests from lxml import etree html = requests.get("http://www.imooc.com/course/list?c=android&page=2") html.encoding = 'utf-8' selector = etree.HTML(html.text) content = selector.xpath('//*[@id="main"]/div/div/div[3]/div[1]/ul/li[1]/a/h5/span/text()') for each in content: print each 这样我们就获取了对应的内容, 搜索方法,其实跟我们通过地址来定位是一样的,中国-上海-浦东新区(内容唯一时,前面可跳过)-张江高科-沪江网-徐宜生 那么如果我们需要爬取所有的课程信息要怎么办呢?我们可以看见生成的XPath中,有一个li[1],它对应的是我们源代码中的那个列表,我们选择1是因为选择具体的一项,如果我们去掉这个1,返回的就是一个列表,就是我们要的所有元素,这里就不详细演示了。 XPath高级使用技巧 相同字符串开头、但属性不同 例如: <div id="test-1">需要的内容1</div> <div id="test-2">需要的内容2</div> <div id="test-3">需要的内容3</div> 我们需要提前所有内容,但是他们的属性都不同,我们先看下一个元素的XPath: //*[@id="test-1"]/text() 可见,ID决定了元素,所以要取出这样的元素时,我们需要使用XPath的starts-with(@属性名称, 属性字符相同部分)方法: //*[starts-with(@id,"test")]/text() 只需要将[]中的内容使用starts-with方法进行匹配就OK了。 嵌套标签 例如: <div id=“class”>text1 <font color=red>text2</font> text3 </div> 类似这种嵌套标签的,如果我们使用XPath获取第一级的text,那么只能获取text1和text3,如果要获取text2,我们就需要使用string(.)方法。 data = selector.xpath('//div[@id="class"]')[0] info = data.xpath('string(.)') content = info.replace('\n','').replace(' ','') print content 通过string(.),我们可以获取嵌套标签的text,相当于遍历子标签,获取text。 最后 写本篇博客的目的在于后面要进行的一次抽奖活动,大家都知道,我的新书《Android群英传》已经正式上市了,为了报答各位的大力推荐,我准备在CSDN博客准备一次抽奖,这篇博客所讲的,自然是抽奖所需要的预备知识,欢迎大家预热~~~
一扫天下——ZXing使用全解析 二维码现在已经烂App了,不管什么App,没有二维码就好像低人一等了。所以,在自己的项目中集成二维码功能还是非常有必要的。 网上很多都是基于ZXing2.3的,但是现在都3.1了,改了很多bug,也进行了很多优化,最好按本文弄一下。 参拜ZXing ZXing的github地址: https://github.com/zxing/zxing 通过git clone git@github.com:zxing/zxing.git 命令我们可以把整个ZXing项目拉取下来。 然而这并没有什么卵用。 因为ZXing的项目是非常庞大的,功能也非常多,但是我们不需要这么多,我们只关心Android部分的。 获得ZXing的祝福之jar 在使用ZXing之前,我们需要先编译它的jar包,我们可以看见源代码目录中有一个core的目录,我们可以把这个文件夹导入eclipse作为一个java工程。最后通过export导出一个jar包。 获得ZXing核心功能 我们需要把核心的扫码、解码功能抽取出来,这一步,网上已经有很多人做过了,只是大部分都是基于第一个抽ZXing的人,而那个是基于ZXing1.5、2.3的,所以,记得要进行Update哦。 那么我们如何获得最新的ZXing代码呢,很简单,找一个旧的,然后把最新的代码一个个copy过去替换就好了,当然,还是会有一些问题,不过一步步解决就可以了,都不是很大的问题。 饭来张口 这里为大家也提供一个封装好的最新的ZXing Lib: https://github.com/xuyisheng/ZXingLib 基于ZXing3.1封装,包含了最新的jar包和代码。 解析 CaptureActivity ZXing暴露的调用Activity。在handleDecode方法中对扫码成功后的动作作处理。 ViewfinderView ZXing扫码窗口的绘制,原始的ZXing使用这种方式去绘制,在上面提供的开源库中,作者将扫描框的绘制直接抽取到了XML文件中,这样修改起来更加方便了。 CameraConfigurationManager 修改横竖屏、处理变形效果的核心类。 在public void setDesiredCameraParameters(Camera camera, boolean safeMode)方法中(读取配置设置相机的对焦模式、闪光灯模式等等),可以将扫描改为竖屏: 即: 在方法最后加上: /** 设置相机预览为竖屏 */ camera.setDisplayOrientation(90); 即可。 在public void initFromCameraParameters(Camera camera)方法中(计算了屏幕分辨率和当前最适合的相机像素),我们可以对修改为竖屏扫码后,由于像素信息点没有对调造成图像扭曲变形进行修改。 即: 在Log.d(TAG, “Screen resolution: ” + screenResolution);后加上如下的代码: /** 因为换成了竖屏显示,所以不替换屏幕宽高得出的预览图是变形的 */ Point screenResolutionForCamera = new Point(); screenResolutionForCamera.x = screenResolution.x; screenResolutionForCamera.y = screenResolution.y; if (screenResolution.x < screenResolution.y) { screenResolutionForCamera.x = screenResolution.y; screenResolutionForCamera.y = screenResolution.x; } 最后,将screenResolution替换为screenResolutionForCamera: cameraResolution = findBestPreviewSizeValue(parameters, screenResolutionForCamera); DecodeHandler.decode ZXing解码的核心类 CaptureActivityHandler 当DecodeHandler.decode完成解码后,系统会向CaptureActivityHandler发消息。如果编码成功则调用CaptureActivity.handleDecode方法对扫描到的结果进行分类处理。 最后 本文的Github中已经包含了前面所提到的所有修改(横竖屏、扭曲变形),用最新的ZXing代码进行了update,同时提供了编码、解码方法,并且将扫码界面抽取成XML(感谢开源作者),方便拓展。 https://github.com/xuyisheng/ZXingLib https://github.com/xuyisheng/ZXingLib https://github.com/xuyisheng/ZXingLib 重要的东西发三遍。 以上。
CSDN极客头条使用指南 今天给大家介绍一下CSDN博客最新推出的这个栏目——CSDN极客头条。 极客头条是什么 极客头条大家分享优质IT资源的聚集地。大家不仅可以分享CSDN的文章,更可以将其他社区的好的文章,在CSDN极客头条这个平台上让更多的开发者知晓。利用CSDN的巨大影响力,让这些优质博文能够获得更多的关注。 互联网社区非常的多,好的技术文章却经常无法被很多的开发者发现,这也是现在为什么很多社区都会成立一些翻译社区,去翻译一些国外的技术博客,目的就是让这些优质资源能够广泛传播。 如何使用 面向专家 极客头条目前是由博客专家进行文章推荐的,相信广大博客专家推荐的文章一定是具有一定专业代表性的。那么要如何进行推荐呢?相信很多博客专家已经发现了,在现在的文章下面,有一个这样的链接: 博客专家在这里点击“推荐到极客头条”就可以一键推荐到极客头条了。使用还是非常简单的。但是这里只限制于使用CSDN博客内的文章,那么如何推荐站外的优质文章呢? 我们可以访问这个链接: http://geek.csdn.net/?ref=toolbar_logo 这个是极客头条的首页,我们点击右边三个按钮中的中间一个,选择发布一条极客头条: 然后输入头条的标题,链接就可以发送了。 当然,还有一种更简单的方法,看到下面的提升了吗,将那个脚本拖到书签栏,下次在想推荐的页面上,直接点这个书签,就可以一键推送了: 注意,下面可以选择发布的具体子栏目,别都发默认的Geek头条这个栏目了。 面向开发者 由于目前只能是博客专家进行推荐,对于普通开发者来说,经常去浏览这些专家推荐的文章,也是非常有好处的,可以知道目前业界的流向。 当我们进入极客头条的主页后: 我们可以像CSDN论坛那样添加要关注的子社区,如果你是全栈,那么当然可以关注全面的子社区哈。像我们这种Android开发者,选择关注Android开发者频道就可以啦。 这里也给大家推荐一下,Android开发者这个子社区是我建立的,希望大家能多多关注这个子社区,多向这个社区贡献自己推荐的好文章,有意的朋友可以向我申请管理,这边已经有好几个活跃的管理啦。我们的目标,就是让广大开发者能够不断接受新的、好的开发者文章。 使用感受 顶 首先,CSDN推出的新版极客头条确实是一个非常好的平台,让大家都能够推广好的IT资源,就像一个开源项目,让大家都来共同维护。 开发者可以像浏览新闻一样,每天只用花很少的时间就可以看到一些非常好的IT资源,而且不限于是CSDN的。 吐槽 CSDN极客头条作为刚上线的产品,有问题肯定是难免的,这当然需要一个迭代的过程,相信同为开发者的读者,应该都懂的。 首先,交互上不够明白。建议把内容设计成新闻头条的样式,不仅有标题,一些具有吸引力的内容也应该适当的展示,最好让极客头条成为开发者利用空余时间逛新闻的地方,比较有头条的感觉。(个人产品倾向,不代表CSDN产品定位) 在子社区内容较少的时候,直接进入子社区,默认显示最热文章,但是由于是最新的,所以没有最热文章显示,感觉体验不太好,最好是能够默认显示最新文章,让用户去调整 子社区不能全部自定义,很多开发者其实并不是全栈工程师,相对比较赶兴趣的,只有自己职位内的新闻,所以,最好让用户能够完全做主。另外,官方可以推荐很多业内的新闻,不一定是技术内的,发展趋势、大事记,都是非常好的内容,这些让CSDN维护,而开发者专注于技术类推荐 使用指南。最好能像MarkDown的使用指南那样形成一个比较系统的使用指南,这样能够极大降低使用成本,虽然我这篇文章的前半部分已经偷偷把这件事做掉了;-) 内容审核要高,既然是头条,那就一定要是值得上头条的东西,所以必须要有价值,不是CSDN上随便搜搜就能搜到的东西。 多端适配。现在的趋势,移动代替PC,CSDN一定要好好转型啊,如何能够在移动端也能很好的使用这些东西,是非常重要的,但是,现在好像做的还很不够。 前端设计,前端设计,没有突出极客二字,黑白界面,对比度太高,反正我也不懂设计,但是这样的设计,不是开发者的设计。 总的来说 总的来说,CSDN这个产品还是很不错的,关键在于提供了这样一个平台,让全民参与,成为了一个极好的开源项目。虽然现在也存在一些问题,但是基本不影响使用了,希望大家在休息的时候,没事的时候,多来看看IT人自己的头条。 以上。
Git workflow 大神镇楼: 这人不用说,应该都认识,他基本干了两件事,一个是Linux,一个就是git。每一件事,都在IT史上创建了一个巨大的Tag。 Git是什么 Git能干什么? Git用来做版本控制,这就好比当初在小日本的公司,每次修改文件,都需要在文件第一页新增修改履历,详细记录修改内容,如果多人同时操作,你可以想象下维护的成本有多高。基本每天就在整理这些破事。 所以说,版本控制是项目开发的重中之重。那么问题来了,版本控制哪家强?一句话,Git是目前世界上最先进的分布式版本控制系统(没有之一)。其实也可以把分布式三个字去掉。 集中式版本控制 集中式的版本控制工具以SVN为代表,他有一个中央服务器,控制着所有的版本管理,其它所有的终端,可以对这个中央库进行操作,中央库保证版本的唯一性。 这样有一个非常不好的地方,就是如果中央服务器被天灾军团攻陷了,那么整个版本就gg了。因为终端只能对一部分代码进行修改,获取各种信息,需要不断与中央服务器通信。 容灾性差 通信频繁 分布式版本控制 分布式版本控制的典型,就是Git,它跟集中式的最大区别就是它的终端可以获取到中央服务器的完整信息,就好像做了一个完整的镜像,这样,我可以在终端做各种操作、获取各种信息而不需要与服务器通信,同时,就算服务器被炸,各个终端还有完整的备份。分布式的思路,在版本控制上,具有得天独厚的优势,当然,这只是git优势的冰山一角。 Git安装与配置 安装 Git的安装就不解释了,相信程序猿都有能力独立解决这样一个问题,Linux和Mac下基本都已经自带Git了,window,建议不要用Git了,命令行实在是受不了。 配置 在终端输入: ~ git --version git version 1.8.4 来查看当前Git的版本,同时,输入: ~ git config --list --global user.name=xuyisheng user.email=xuyisheng@hujiang.com push.default=current 或者: ~ git config user.name xuyisheng 用来查看当前的配置信息,如果是新安装的,那么需要对global信息进行配置。配置的方法有两种,一个个变量配置,或者一起配置: 单独配置: ~ git config --global user.name xys 一起配置: ~ git config --global --add user.name xys 增加多个键值对 删除配置 ~ git config --global --unset user.name xys 配置别名 这个功能在shell中是很常用的。我们可以做一些别名来取代比较复杂的指令。 比如: git config --global alias.st status 我们使用st来取代status。 附上一个比较吊的: git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" PS:用git log –graph命令可以看到分支合并图。 配置文件 git的配置文件其实我们是可以找到的,就在.git目录下: testGit git:(master) ls -a . .. .git README.txt testGit git:(master) cd .git .git git:(master) ls HEAD description index logs packed-refs config hooks info objects refs .git git:(master) 我们打开config文件: [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = false [remote "origin"] url = git@github.com:xuyisheng/testGit.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master [branch "dev"] remote = origin merge = refs/heads/dev 这里就是我们所有的配置了。 创建Git仓库 版本控制就是为了管理代码,代码就要放在仓库中。创建仓库有二种方法: Git init MyWork mkdir gitTest MyWork cd gitTest gitTest git init Initialized empty Git repository in /Users/hj/Downloads/MyWork/gitTest/.git/ gitTest git:(master) 创建一个目录,并cd到目录下,通过调用git init来将现有目录初始化为git仓库,或者直接在git init后面跟上目录名,同样也可以创建一个新的仓库。 git clone git clone用于clone一个远程仓库到本地,这个我们后面再将。 创建好仓库后,目录下会生成一个.git的隐藏文件夹,这里就是所有的版本记录,默认不要对这个文件夹进行修改。 提交修改 add && commit 在仓库中,我们创建代码,并将其提交: gitTest git:(master) touch README.txt gitTest git:(master) open README.txt gitTest git:(master) git add README.txt gitTest git:(master) git commit -m "add readme" [master (root-commit) c19081b] add readme 1 file changed, 1 insertion(+) create mode 100644 README.txt 我们创建了一个README文件,并通过git add <文件名>的方式进行add操作,最后通过git commit操作进行提交,-m参数,指定了提交的说明。 这两个命令应该是最常使用的git命令。 查看修改 在版本控制中,非常核心的一点,就是需要指定,我们做了哪些修改,比如之前我们创建了一个README文件,并在里面写了一句话: this is readme。 下面我们修改这个文件: this is readme,modify。 接下来,我们使用git status命令来查看当前git仓库哪些内容被修改了: gitTest git:(master) git status # On branch master # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: README.txt # no changes added to commit (use "git add" and/or "git commit -a") 我们可以发现,git提示我们: modified: README.txt,更进一步,我们可以使用git diff命令来查看具体的修改: diff --git a/README.txt b/README.txt index 2744f40..f312f1a 100644 --- a/README.txt +++ b/README.txt @@ -1 +1 @@ -this is readme \ No newline at end of file +this is readme, modify \ No newline at end of file (END) 这样就查看了具体的修改。 下面我们继续add、commit: gitTest git:(master) git add README.txt gitTest git:(master) git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: README.txt # gitTest git:(master) git commit -m "modify readme" [master 1629cdc] modify readme 1 file changed, 1 insertion(+), 1 deletion(-) gitTest git:(master) git status # On branch master nothing to commit, working directory clean add和commit之后,我们都使用status来查看下状态,可以发现,在commit之后,git提示我们,工作区是干净的。 版本记录 在项目中,一个仓库通常可能有非常多次的add、commit过程,这些记录都会被记录下来,我们可以使用git log来查看这些记录: commit 1629cdcf2307bf26c0c5467e10035c2bd751e9d0 Author: xuyisheng <xuyisheng@hujiang.com> Date: Sun Jun 28 14:45:14 2015 +0800 modify readme commit c19081b6a48bcd6fb243560dafc7a35ae5e74765 Author: xuyisheng <xuyisheng@hujiang.com> Date: Sun Jun 28 14:35:00 2015 +0800 add readme (END) 每条记录都对应一个commit id,这是一个40个16进制的sha-1 hash code。用来唯一标识一个commit。 同时,我们也可以使用gitk命令来查看图形化的log记录: git会自动将commit串成一个时间线。每个点,就代表一个commit。点击这些点,就可以看见相应的修改信息。 工作区与暂存区 Git通常是工作在三个区域上: 工作区 暂存区 历史区 其中工作区就是我们平时工作、修改代码的区域,而历史区,用来保存各个版本,而暂存区,则是Git的核心所在。 暂存区保存在我们前面讲的那个.git的隐藏文件夹中,是一个叫index的文件。 当我们向Git仓库提交代码的时候。add操作实际上是将修改记录到暂存区,我们来看gitk: 可以发现,我们在本地已经生成了一个记录,但是还没有commit,所以当前HEAD并没有指向我们的修改,修改还保存在暂存区。 当我们commit之后,再看gitk: 这时候,HEAD就已经移到了我们的修改上,也就是说,我们的提交生成了一个新的commit。git commit操作就是将暂存区的内容全部提交。如果内容不add到暂存区,那么commit就不会提交修改内容。 PS 这里需要说明一个概念,git管理的是修改,而不是文件,每个sha-1的值,也是根据内容计算出来的。 版本回退 如果这个世界一直只需要git add、commit,就不会有这么多的问题了,但是,愿望是美好的,现实是残酷的。如何处理版本的回退和修改,是使用git非常重要的一步。 checkout && reset 我们来考虑几种情况: 文件已经修改,但是还没有git add 文件已经add到暂存区,又作了修改 文件的修改已经add到了暂存区 分别执行以下操作: gitTest git:(master) git checkout -- README.txt 修改被删除,完全还原到上次commit的状态,也就是服务器版本 最后的修改被删除,还原到上次add的状态,也就是修改前的暂存区状态 总的来说,就是还原到上次add、commit的状态。 而对于第三种情况,我们可以使用git reset: gitTest git:(master) git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: README.txt # gitTest git:(master) git reset HEAD README.txt Unstaged changes after reset: M README.txt gitTest git:(master) git status # On branch master # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: README.txt # no changes added to commit (use "git add" and/or "git commit -a") 通过git reset HEAD README.txt,我们就把暂存区的文件清除了,这样,在本地就是add前的状态,通过checkout操作,就可以进行修改回退了。 PS : git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除 回退版本 当我们的仓库有了大量的提交的时候,我们可以通过git log来查看(可以指定 –pretty=oneline 来优化显示效果)。 e7ae095cafc8ddc5fda5a5d8b23d0bcaaf74ac39 modify again 5427c66703abfeaba3706f938317251ef2567e8b delete test.txt 08098a21a918cfbd6377fc7a03a08cac0e6bcef6 add new file b687b06fbb66da68bf8e0616c8049f194f03a062 e 8038c502e6f5cbf34c8096eb27feec682b75410b update 34ad1c36b97f090fdf3191f51e149b404c86e72f modify again 1629cdcf2307bf26c0c5467e10035c2bd751e9d0 modify readme c19081b6a48bcd6fb243560dafc7a35ae5e74765 add readme 那么我们如果回退到指定版本呢?在Git中,用HEAD表示当前版本,上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本就不要这样写了,写成HEAD~100即可。 下面我们就可以回退了: testGit git:(master) git reset --hard HEAD^ HEAD is now at 5427c66 delete test.txt 要回退到哪个版本,只要HEAD写对就OK了。你可以写commit id,也可以HEAD^,也可以HEAD^^。 前进版本 有时候,如果我们回退到了旧的版本,但是却后悔了,想回到后面某个新的版本,但这个时候,我的commit id已经忘了,怎么办呢? 没事,通过这个指令: 5427c66 HEAD@{0}: reset: moving to HEAD^ e7ae095 HEAD@{1}: checkout: moving from dev to master 7986a59 HEAD@{2}: checkout: moving from master to dev e7ae095 HEAD@{3}: checkout: moving from dev to master 7986a59 HEAD@{4}: checkout: moving from 7986a59bd8683acb560e56ff222324cd49edb5e5 to dev 7986a59 HEAD@{5}: checkout: moving from master to origin/dev e7ae095 HEAD@{6}: clone: from git@github.com:xuyisheng/testGit.git 这样我们就可以找到commit id用来还原了。 git reflog记录的就是你的操作历史。 文件操作 git rm 如果我们要删除git仓库中的文件,我们要怎么做呢? 我们创建一个新的文件并提交,再删除这个文件: gitTest git:(master) rm test.txt gitTest git:(master) git status # On branch master # Changes not staged for commit: # (use "git add/rm <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # deleted: test.txt # no changes added to commit (use "git add" and/or "git commit -a") git提示我们delete一个文件,这时候你可以选择: gitTest git:(master) git checkout test.txt 这样就撤销了删除操作,或者使用: gitTest git:(master) git rm test.txt rm 'test.txt' gitTest git:(master) git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # deleted: test.txt # gitTest git:(master) git commit -m "delete test.txt" [master 5427c66] delete test.txt 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test.txt 通过git rm指令,删除文件,最后提交修改,就真实的删除了文件。 文件暂存 这里的暂存不是前面说的暂存区,而是只一次备份与恢复操作。举个例子,当前我们在dev分支上进行一个新功能的开发,但是开发到一半,测试提了一个issue,这时候,我们需要创建一个issue分支来修改这个bug,但是,当前dev分支是不干净的,新功能开发到一半,直接从dev上拉分支,代码是不完善的,可能会编译不过。 so,你可以使用: git stash 指令来将当前修改暂存,这样就可以切换到其他分支或者就在当前干净的分支上checkout了。 比如你checkout了一个issue分支,修改了bug,使用git merge合并到了master分支,删除issue分支,切换到dev分支,想继续之前的新功能开发。 这时候,就需要恢复现场了: 首先,通过: git stash list 我们可以查看当前暂存的内容记录。 然后,通过git stash apply或者git stash pop来进行恢复,它们的区别是,前者不会删除记录(当然你可以使用git stash drop来删除),而后者会。 远程仓库 既然git是分布式的仓库管理,那么我们肯定是需要多台服务器进行各种操作的,一般在开发中,我们会用一台电脑做中央服务器,各个终端从中央服务器拉取代码,提交修改。 那么我们如何去搭建一个git远程服务器呢,答案是不要搭建,个人开发者可以通过github来获取免费的远程git服务器,或者是国内的开源中国之类的,同样也提供免费的git服务器,而对于企业用户,可以通过gitlab来获取git远程服务器。 身份认证 当本地git仓库与git远程仓库通信的时候,需要进行SSH身份认证。 打开根目录下的.ssh目录: ~ cd .ssh .ssh ll total 40 -rw------- 1 xys staff 1.7K 5 15 23:53 github_rsa -rw-r--r-- 1 xys staff 402B 5 15 23:53 github_rsa.pub -rw------- 1 xys staff 1.6K 5 15 09:38 id_rsa -rw-r--r-- 1 xys staff 409B 5 15 09:42 id_rsa.pub -rw-r--r-- 1 xys staff 2.0K 6 3 13:34 known_hosts 如果没有id_rsa和id_rsa.pub这两个文件,就通过如下的命令生成: ssh-keygen -t rsa -C "youremail@example.com" id_rsa和id_rsa.pub这两个文件,就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,用在github上表明身份。 在github的ssh key中增加刚刚生成的key: 同步协作 现在你在本地建立了git仓库,想与远程git仓库同步,这样github的远程仓库可以作为你本地的备份,也可以让其他人进行协同工作。 我们先在github上创建一个repo(仓库): 创建之后,github给我们提示: github告诉了我们如何在本地创建一个新的repo或者将既存的repo提交到远程git。 由于我们已经有了本地的git,所以,安装提示: gitTest git:(master) git remote add origin git@github.com:xuyisheng/testGit.git gitTest git:(master) git push -u origin master Counting objects: 18, done. Delta compression using up to 4 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (18/18), 1.39 KiB | 0 bytes/s, done. Total 18 (delta 1), reused 0 (delta 0) To git@github.com:xuyisheng/testGit.git * [new branch] master -> master Branch master set up to track remote branch master from origin. 现在我们再看github上的仓库: README已经提交上去了。 git remote add origin git@github.com:xuyisheng/testGit.git这条指令中的origin,就是远程仓库的名字,你也可以叫别的,但是默认远程仓库都叫做origin,便于区分。 PS: 这里还需要注意下的是git push的-u参数,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来。不过后面的push就不需要这个参数了。 之后我们再做修改: gitTest git:(master) git add README.txt gitTest git:(master) git commit -m "modify again" [master e7ae095] modify again 1 file changed, 2 insertions(+), 1 deletion(-) gitTest git:(master) git push Counting objects: 5, done. Delta compression using up to 4 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 285 bytes | 0 bytes/s, done. Total 3 (delta 0), reused 0 (delta 0) To git@github.com:xuyisheng/testGit.git 5427c66..e7ae095 master -> master 可以直接使用git push或者git push origin master来指定仓库和分支名。 clone远程仓库 记得我们之前说的,创建本地仓库的几种方式,其中有一种是clone: 我们可以通过git clone指令来clone一个远程仓库: 下面就显示了远程仓库的地址,一般我们使用SSH的方式,当然,也可以使用其他方式,然并卵。 PS: 使用https除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh协议而只能用https。 直接使用: MyWork git clone git@github.com:xuyisheng/testGit.git Cloning into 'testGit'... remote: Counting objects: 21, done. remote: Compressing objects: 100% (9/9), done. remote: Total 21 (delta 1), reused 21 (delta 1), pack-reused 0 Receiving objects: 100% (21/21), done. Resolving deltas: 100% (1/1), done. Checking connectivity... done 分支管理 个人认为,创建分支是git最大的魅力。 git中的分支就好像现在的平行宇宙,不同的分支互不干扰,相互独立,你就像一个上帝一样,可以随时对任意一个分支进行操作,可以今天去这个branch玩,明天去另一个branch,玩腻了,再把两个分支合并,一起玩。 举个比较恰当的例子,我现在要开发一个新功能,需要3个月的时间,但是我不能每天都把未完成的代码提交到大家都在用的分支上,这样人家拉取了我的代码就无法正常工作了,但是我又不能新建一个仓库,这也太浪费了,所以我可以新建一个分支,在这个分支上开发我的功能,同时能够备份我的代码,当开发完毕后,直接合并分支,整个新功能就一下子加入了大家的分支。 创建分支 你的每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。不创建分支时,只有一条时间线,在Git里,这个分支叫主分支,即master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。 这里的时间线就看的非常清楚了。 我们通过如下指令来创建分支: gitTest git:(master) git checkout -b dev Switched to a new branch 'dev' gitTest git:(dev) -b参数代表创建并切换到该分支,相当于: $ git branch dev $ git checkout dev Switched to branch 'dev' 不加-b参数就是直接切换到已知分支了。 查看分支 通过git branch指令可以列出当前所有分支: gitTest git:(dev) git branch * dev master 当前分支上会多一个* 合并分支 切换到dev分支后,我们进行修改,add并commit: 此时我们再切换到master分支,再查看当前修改,你会发现,dev分支上做的修改这里都没有生效。必须的,不然平行宇宙就不平行了。我们通过如下指令来进行分支的合并: gitTest git:(master) git merge dev Updating e7ae095..7986a59 Fast-forward README.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 这样再查看master分支下的文件,dev上的修改就有了。 删除分支 使用完毕后,我们不再需要这个分支了,所以,放心的删除吧: gitTest git:(master) git branch -d dev Deleted branch dev (was 7986a59). gitTest git:(master) git branch * master PS : 当分支还没有被合并的时候,如果删除分支,git会提示: error: The branch ‘feature-vulcan’ is not fully merged. If you are sure you want to delete it, run ‘git branch -D dev. 也就是提示我们使用-D参数来进行强行删除。 看完分支的操作,有人可能会问,创建这么多分支,git会不会很累,当然不会,git并不是创建整个文件的备份到各个分支,而是创建一个指针指向不同的分支而已。切换分支,创建分支,都只是改变指针指向的位置。 分支是非常好的团体协作方式,一个项目中通常会有一个master分支来进行发布管理,一个dev分支来进行开发,而不同的开发者checkout出dev分支来进行开发,merge自己的分支到dev,当有issue或者新需求的时候,checkout分支进行修改,可以保证主分支的安全,即使修改取消,也不会影响主分支。 查看远程分支 当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。 通过如下指令,我们可以查看远程分支: gitTest git:(master) git remote origin 或者: gitTest git:(master) git remote -v origin git@github.com:xuyisheng/testGit.git (fetch) origin git@github.com:xuyisheng/testGit.git (push) 来显示更详细的信息。 推送分支 要把本地创建的分支同步到远程仓库上,我们可以使用: gitTest git:(master) git checkout -b dev Switched to a new branch 'dev' gitTest git:(dev) git push origin dev Everything up-to-date 这样就把一个dev分支推送到了远程仓库origin中。 抓取远程分支 当我们将远程仓库clone到本地后即使远程仓库有多个分支,但实际上,本地只有一个分支——master。 MyWork git clone git@github.com:xuyisheng/testGit.git Cloning into 'testGit'... remote: Counting objects: 24, done. remote: Compressing objects: 100% (11/11), done. remote: Total 24 (delta 2), reused 23 (delta 1), pack-reused 0 Receiving objects: 100% (24/24), done. Resolving deltas: 100% (2/2), done. Checking connectivity... done MyWork cd testGit testGit git:(master) git branch * master 现在,我要在dev分支上开发,就必须创建远程origin的dev分支到本地,用这个命令创建本地dev分支: testGit git:(master) git checkout -b dev origin/dev Branch dev set up to track remote branch dev from origin. Switched to a new branch 'dev' testGit git:(dev) 这样就把本地创建的dev分支与远程的dev分支关联了。 后面你可以使用git push origin dev继续提交代码到远程的dev分支。 这种情况下,如果其他人也提交了到dev分支,那么你的提交就会与他的提交冲突,因此,你需要使用git pull先将远程修改拉取下来,git会自动帮你在本地进行merge,如果没有冲突,这时候再提交,就OK了,如果有冲突,那么必须手动解除冲突再提交。 Tag Tag的概念非常类似于branch。但是branch是可以不断改变、merge的,Tag不行,Tag可以认为是一个快照,用于记录某个commit点的历史快照。 创建标签 非常简单: testGit git:(master) git tag version1 默认Tag会打在最后的提交上。但是你也可以通过commit id来指定要打的地方。 testGit git:(master) git tag version0 b687b06fbb66da68bf8e0616c8049f194f03a062 testGit git:(master) git tag version0 version1 PS : 实际上commit id不需要写很长,通过前6、7位,git就可以查找到相应的id了。 还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字: git tag -a v1 -m "version1" b687b06fbb66da68bf8e0616c8049f194f03a062 通过如下指令来查看详细信息: git show <tagname> 查看标签 testGit git:(master) git tag version1 删除标签 -d参数就可以了,与branch类似。 testGit git:(master) git tag version0 version1 testGit git:(master) git tag -d version0 Deleted tag 'version0' (was b687b06) testGit git:(master) git tag version1 推送到远程 将本地tag推送到远程参考上: testGit git:(master) git push origin version0 Total 0 (delta 0), reused 0 (delta 0) To git@github.com:xuyisheng/testGit.git * [new tag] version0 -> version0 或者: testGit git:(master) git push origin --tags Total 0 (delta 0), reused 0 (delta 0) To git@github.com:xuyisheng/testGit.git * [new tag] version1 -> version1 来push所有的tag。 删除远程Tag 当Tag已经push到远程仓库后,我们要删除这个Tag,首先需要先删除本地Tag: testGit git:(master) git tag -d version0 Deleted tag 'version0' (was b687b06) 再push到远程,带指令有所不同: testGit git:(master) git push origin :refs/tags/version0 To git@github.com:xuyisheng/testGit.git - [deleted] version0 以上。 学完这些,对付基本的Git使用,就没什么问题了,后面会再出一篇Git workflow plus,介绍一下Git的高级 操作。
MarkDown编辑器推荐 最近有很大朋友私信我,询问有哪些比较好的Markdown的编辑器,这里做一个汇总哈。 非常赞的在线编辑器 stackedit https://stackedit.io/editor markdown-live-editor http://jrmoran.com/playground/markdown-live-editor/ 老牌编辑器 markdownpad http://markdownpad.com/ Mac下的小清新 Mou http://mouapp.com/ 国产神器 马克飞象 http://www.maxiang.info/ 作业部落 https://www.zybuluo.com/mdeditor CSDN 最后,我想说,用了这么多MD的编辑器,最好用的还是CSDN的MD编辑器,方便、简洁,功能完善。 真心推荐。 以上。
Git在线练习 http://pcottle.github.io/learnGitBranching/ https://try.github.io/levels/1/challenges/1 Git入门 http://code.tutsplus.com/tutorials/easy-version-control-with-git–net-7449 http://blog.jobbole.com/25775/ http://rogerdudler.github.io/git-guide/index.zh.html 图解Git http://marklodato.github.io/visual-git-guide/index-zh-cn.html 看完这些内容,Git基本操作无障碍啊。
Android Camera探究之路——起步 Camera在手机中有着举足轻重的地位,不管是二维码还是照片、识别,都离不开摄像头,本文将对Android中的Camera进行全面解析。 权限镇楼: <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-feature android:name="android.hardware.camera"/> 调用系统Camera 通过系统定义的Intent Action,我们可以很方便的使用所有实现了Camera功能的App。 ACTION_IMAGE_CAPTURE 这个action是最常用的一个调用系统Camera的action。 使用方式如下: Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 通过这样一个Action,我们就可以调用所有声明了Camera的App。 那么如何收到拍摄的图片呢?我们自然是需要使用startActivityForResult方法。 这里我们先来看最简单的: 我们在: @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); } onActivityResult方法中,通过data参数来获取图像: /** * 通过data取得图片 */ Bundle extras = data.getExtras(); Bitmap bitmap = (Bitmap) extras.get("data"); mImageViewShow.setImageBitmap(bitmap); 但是,现在手机像素这么高,万一图片特别大呢,会不会data过大而FC呢?放心,Android早就考虑到了,所以,data里面压根就不是完整的图片,它只是一张缩略图,对,真的是缩略图。所以,我们需要获取到拍摄的原图,就不能使用这种方法。但是我们可以这样做,我们可以指定MediaStore类的一个EXTRA_OUTPUT来指定拍摄图像保存的位置,相当于建立一个临时文件。在onActivityResult中,我们不使用data来获取图像,而是直接去读这个临时文件即可。 指定EXTRA_OUTPUT: String tempPath = Environment.getExternalStorageDirectory().getPath(); mFilePath = tempPath + "/" + "test1.png"; Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Uri photoUri = Uri.fromFile(new File(mFilePath)); intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); startActivityForResult(intent, CAMERA_CODE1); onActivityResult: /** * 通过暂存路径取得图片 */ FileInputStream fis = null; Bitmap bitmap = null; try { fis = new FileInputStream(mFilePath); bitmap = BitmapFactory.decodeStream(fis); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } 这样我们就可以获取到完整的拍摄图片了。后面你可以让图像显示出来,显示的时候,同样需要考虑大图的处理,避免图像尺寸带来的问题,这些东西,请参考这里: http://blog.csdn.net/eclipsexys/article/details/44459771 这里就不赘述了。如果你的App仅仅是需要非常简单的拍摄功能,那么通过调用系统Intent就足够了,但是大部分时候,这都是不可能的,所以下面我们来看看如何自定义Camera。 自定义Camera 根据Google Android Doc,自定义一个Camera需要如下几个步骤: 1.检查Camera是否存在,并在AndroidManifest.xml中赋予相关的权限; 2.创建一个继承于SurfaceView并实现SurfaceHolder接口的Camera Preview类; 3.新建一个Camera Preview布局文件; 4.设置一个拍照的监听事件,例如单击按钮事件等; 5.实现拍照,并保存拍照后的图片到设备; 6.释放Camera。 看上去还是比较复杂的。所以我们一步步来。 首先,我们创建预览Camera的界面: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/ll" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:orientation="horizontal"> <Button android:id="@+id/btn_switch_camera" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:onClick="switchCamera" android:text="切换摄像头"/> <Button android:id="@+id/btn_capture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:onClick="capture" android:text="拍照"/> </LinearLayout> <SurfaceView android:id="@+id/sv_camera" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/ll" android:text="camera"/> </RelativeLayout> 非常简单,两个button下面一个surfaceview: 然后,我们创建一个Activity,用来展示Camera的预览: 这个Activity里面肯定有SurfaceView,所以,SurfaceView的那一套东西,自然是少不了,不懂的请自行脑补。 那么在这个Activity里面,我们需要做什么呢?两件事情: 初始化相机 将内容显示到SurfaceView Android的Camera是独享的,如果多处调用,就会抛出异常,所以,我们需要将Camera的生命周期与Activity的生命周期绑定: onResume方法中初始化相机 onPause方法中释放相机 初始化相机非常简单: /** * 初始化相机 * * @return camera */ private Camera getCamera() { Camera camera; try { camera = Camera.open(); } catch (Exception e) { camera = null; } return camera; } 释放相机也非常简单: /** * 释放相机资源 */ private void releaseCamera() { if (mCamera != null) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } 那么下面我们再来看如何把相机图像设置到SurfaceView中进行预览: /** * 在SurfaceView中预览相机内容 * * @param camera camera * @param holder SurfaceHolder */ private void setStartPreview(Camera camera, SurfaceHolder holder) { try { camera.setPreviewDisplay(holder); camera.setDisplayOrientation(90); camera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } 尼玛,是不是也非常简单,camera的一个方法已经帮我们自动关联了SurfaceView。 PS 这里需要注意下这个方法camera.setDisplayOrientation(90),通过这个方法,我们可以调整摄像头的角度,不然默认是横屏,图像会显示的比较奇怪。当然,即使你设置的90,图像也有可能比较奇怪,这是因为你没有对图像进行正确的缩放,比例不对。 通过上面的设置,我们已经可以正常预览摄像头的图像内容了,下面我们就可以拍照了。 唉,拍照真的也非常简单,就一句话: mCamera.takePicture(null, null, mPictureCallback); 当然,为了配合拍照,我们需要做一些设定,设置拍照的参数,并且给拍照之后的动作设定一个回调: 参数: Camera.Parameters params = mCamera.getParameters(); params.setPictureFormat(ImageFormat.JPEG); params.setPreviewSize(800, 400); params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); mCamera.setParameters(params); // 使用自动对焦功能 mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { mCamera.takePicture(null, null, mPictureCallback); } }); 回调: /** * Camera回调,通过data[]保持图片数据信息 */ Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { File pictureFile = getOutputMediaFile(); if (pictureFile == null) { return; } try { FileOutputStream fos = new FileOutputStream(pictureFile); fos.write(data); fos.close(); Intent intent = new Intent(CustomCamera.this, CameraResult.class); intent.putExtra("picPath", pictureFile.getAbsolutePath()); startActivity(intent); CustomCamera.this.finish(); } catch (IOException e) { e.printStackTrace(); } } }; 在回调中,我们将拍摄好的图片地址传递给用于展示的ImageView。这样就完成了相机的拍摄与图片的展示。 处理图像变形 由于我们自己在布局中创建了一个SurfaceView,而且我们之间让他match_parent了,所以,图像在preview的时候,肯定是会有拉伸的。那么如何处理这些变形呢? 我们可以通过改变SurfaceView大小的方式来实现,在Android API Demo中,Google也给我们提供了这样一个实例: 路径如下: android-22/legacy/ApiDemos/src/com/example/android/apis/graphics/CameraPreview.java Google就是通过设置新的大小来适应预览区域大小的方式来解决变形问题的,所以说,内事不懂看源码,外事不懂看Demo。 自定义取景画面 听上去非常高大上,其实,真的非常简单,你只需要用一个FrameLayout把用来Preview的SurfaceView包起来就OK了,下面你想加什么,就直接在FrameLayout中加吧,like this: <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/ll"> <SurfaceView android:id="@+id/sv_camera" android:layout_width="match_parent" android:layout_height="match_parent" android:text="拍照区域"/> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="center" android:src="@drawable/demo"/> </FrameLayout> 不光了ImageView,ViewPager也可以,这样甚至可以做一个可切换的水印相机了。是不是非常简单,而且加入的一切都是可操作的,加动效、颜色,分分钟搞定。 以上。 起步之后,我们要开始跑了。 代码下载,请移步全球最大同性程序猿交友社区: https://github.com/xuyisheng/CameraGuide 后续篇章也会在此repo中更新。
Ratingbar UseGuide Ratingbar是一个评分控件,系统给我们提供了这样一个控件,样式如下: 相信大家都见过这样一个控件。本文将详细的讲解Ratingbar的使用和改造。 系统默认Ratingbar RatingBar是基于SeekBar(拖动条)和ProgressBar(状态条)的扩展,用星形来显示等级评定。 我们来看下系统默认的Ratingbar: 这三种Ratingbar是系统给我们提供的样式,代码分别如下: <RatingBar android:id="@+id/origin_ratingbar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:numStars="6" android:rating="3"/> <RatingBar style="?android:attr/ratingBarStyleIndicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:numStars="7" android:rating="3"/> <RatingBar style="?android:attr/ratingBarStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:numStars="8" android:rating="3"/> 第一种为系统默认样式Ratingbar,不做任何改变,完全原生态。 第二种为大样式Ratingbar,使用系统Style “?android:attr/ratingBarStyleIndicator” 第三种为小样式Ratingbar,使用系统Style “?android:attr/ratingBarStyleSmall” 系统给我们提供的Ratingbar基本可以满足我们不高的需求,它提供了一些属性: 属性 作用 android:rating=”3” 当前显示的Star数 android:numStars=”7” 总共的Star数 android:stepSize=”1.5” Star增加时的步长 android:isIndicator=”true” Ratingbar是否可用 都很简单明了,看了就知道怎么用。 PS:很蛋疼的一点,系统的Ratingbar必须使用wrap_content布局,如果match_parent,定义的numStars就失效了。而且,系统的Ratingbar是无法调节Star与Star之间的间距的。 自定义样式Ratingbar 系统的Ratingbar虽然功能满足了,但是实在太丑,Star的样式还是无法控制,所以,我们可以通过Style来控制其样式。 创建Star图片 首先,我们创建一个drawable: <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+android:id/background" android:drawable="@drawable/staroff"/> <item android:id="@+android:id/secondaryProgress" android:drawable="@drawable/staroff"/> <item android:id="@+android:id/progress" android:drawable="@drawable/staron"/> </layer-list> 很简单,指定三个属性: progress:用来在背景图片基础上进行填充的指示属性(和进度条类似,第一进度位置) secondaryProgress:类似progressbar的二级进度条 background:用来填充背景图片,和进度条类似,当我们设置最高Star时(android:numStars),系统就会根据我们的设置,来画出以Star为单位的背景(例如android:numStars=”5”,就会画出5颗灰色的Star) 引用的id在IDE中可能会报错,但是不影响编译。 定义Style 我们创建一个Style,用来定义Ratingbar的样式: <style name="MyRatingBar" parent="@android:style/Widget.RatingBar"> <item name="android:progressDrawable">@drawable/ratingbar_bg</item> <item name="android:minHeight">48dp</item> <item name="android:maxHeight">48dp</item> </style> 通过拓展Widget.RatingBar来自定义样式,并指定其android:progressDrawable参数为我们前面设置的图片样式。 引用Style 最后,我们在代码中引用自定义的Style: <RatingBar style="@style/MyRatingBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:numStars="8" android:rating="3"/> 最后的显示样式如下: 这里也有个蛋疼的地方,那就是,一旦设置了自定义的Star样式、背景,Star在Ratingbar中就无法竖直居中了,所以,只能靠切图时留好边距来调整位置,这样同时也能解决无法定义Star直接间隔的问题。 重写Ratingbar 这必须的,最后我们会发现,系统提供的这个Ratingbar太鸡肋了,实在是不好意思直接拿来用,所以,我们来重写一个Ratingbar。 重写Ratingbar,我们就不使用系统的方式——拓展progressbar的方式。我们创建一个ViewGroup,通过设置不同数量的图片,来控制显示的Star。 创建属性 首先我们自定义attrs属性: <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="RatingBarView"> <attr name="starImageSize" format="dimension"/> <attr name="starCount" format="integer"/> <attr name="starEmpty" format="reference"/> <attr name="starFill" format="reference"/> </declare-styleable> </resources> 定义四个属性,分别用来控制显示Star的大小,数量,未填充的图像,填充的图像。 重写Ratingbar 我们通过继承LinearLayout的方式来实现,往LinearLayout里面塞ImageView。 package com.xys.ratingbarguide; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.animation.ScaleAnimation; import android.widget.ImageView; import android.widget.LinearLayout; public class RatingBarView extends LinearLayout { public interface OnRatingListener { void onRating(Object bindObject, int RatingScore); } private boolean mClickable = true; private OnRatingListener onRatingListener; private Object bindObject; private float starImageSize; private int starCount; private Drawable starEmptyDrawable; private Drawable starFillDrawable; private int mStarCount; public void setClickable(boolean clickable) { this.mClickable = clickable; } public RatingBarView(Context context, AttributeSet attrs) { super(context, attrs); setOrientation(LinearLayout.HORIZONTAL); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RatingBarView); starImageSize = ta.getDimension(R.styleable.RatingBarView_starImageSize, 20); starCount = ta.getInteger(R.styleable.RatingBarView_starCount, 5); starEmptyDrawable = ta.getDrawable(R.styleable.RatingBarView_starEmpty); starFillDrawable = ta.getDrawable(R.styleable.RatingBarView_starFill); ta.recycle(); for (int i = 0; i < starCount; ++i) { ImageView imageView = getStarImageView(context, attrs); imageView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mClickable) { mStarCount = indexOfChild(v) + 1; setStar(mStarCount, true); if (onRatingListener != null) { onRatingListener.onRating(bindObject, mStarCount); } } } }); addView(imageView); } } private ImageView getStarImageView(Context context, AttributeSet attrs) { ImageView imageView = new ImageView(context); ViewGroup.LayoutParams para = new ViewGroup.LayoutParams(Math.round(starImageSize), Math.round(starImageSize)); imageView.setLayoutParams(para); // TODO:you can change gap between two stars use the padding imageView.setPadding(0, 0, 40, 0); imageView.setImageDrawable(starEmptyDrawable); imageView.setMaxWidth(10); imageView.setMaxHeight(10); return imageView; } public void setStar(int starCount, boolean animation) { starCount = starCount > this.starCount ? this.starCount : starCount; starCount = starCount < 0 ? 0 : starCount; for (int i = 0; i < starCount; ++i) { ((ImageView) getChildAt(i)).setImageDrawable(starFillDrawable); if (animation) { ScaleAnimation sa = new ScaleAnimation(0, 0, 1, 1); getChildAt(i).startAnimation(sa); } } for (int i = this.starCount - 1; i >= starCount; --i) { ((ImageView) getChildAt(i)).setImageDrawable(starEmptyDrawable); } } public int getStarCount() { return mStarCount; } public void setStarFillDrawable(Drawable starFillDrawable) { this.starFillDrawable = starFillDrawable; } public void setStarEmptyDrawable(Drawable starEmptyDrawable) { this.starEmptyDrawable = starEmptyDrawable; } public void setStarCount(int startCount) { this.starCount = starCount; } public void setStarImageSize(float starImageSize) { this.starImageSize = starImageSize; } public void setBindObject(Object bindObject) { this.bindObject = bindObject; } public void setOnRatingListener(OnRatingListener onRatingListener) { this.onRatingListener = onRatingListener; } } 代码基本没有什么好说的,非常简单的自定义View。在显示Star的时候,我们还可以添加显示的动画,我这里就只做了一个简单的缩放动画。同时,通过设置imageView的padding,我们可以解决Star之间设置间距的问题。 最后,显示效果如下: 响应事件 Ratingbar的响应事件与progressbar的响应事件类似。通过设置监听来监听Star选择的改变,当然,我们自定义的RatingbarView设置了一个接口,来实现监听: RatingBar customRatingbar = (RatingBar) findViewById(R.id.origin_ratingbar); customRatingbar.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() { @Override public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { Toast.makeText(MainActivity.this, String.valueOf(rating), Toast.LENGTH_SHORT).show(); } }); RatingBarView originRatingbar = (RatingBarView) findViewById(R.id.custom_ratingbar); originRatingbar.setOnRatingListener(new RatingBarView.OnRatingListener() { @Override public void onRating(Object bindObject, int RatingScore) { Toast.makeText(MainActivity.this, String.valueOf(RatingScore), Toast.LENGTH_SHORT).show(); } }); 回调中的参数,就是Star的数量。除了回调,Ratingbar也提供了一些方法来返回Star的数量。 原生: mRatingBar.getMax() mRatingBar.getRating() 自定义: mRatingBarView.getStarCount() 库 这个自定义Ratingbar可以放到单独的库项目中,作为UIKit来进行使用,那么为什么我这里没有呢?原因就是,我比较懒。需要的请自己抽一下,一共就两个文件。 以上,Ratingbar全解析结束,只留下一个Github: https://github.com/xuyisheng/RatingbarGuide 欢迎fork、Star。
ViewDragHelper ——视图拖动是一个比较复杂的问题。这个类可以帮助解决不少问题。如果你需要一个例子,DrawerLayout就是利用它实现扫滑。Flavient Laurent 还写了一些关于这方面的优秀文章。 PopupWindow——Android到处都在使用PopupWindow ,甚至你都没有意识到(标题导航条ActionBar,自动补全AutoComplete,编辑框错误提醒Edittext Errors)。这个类是创建浮层内容的主要方法。 Actionbar.getThemrContext()——导航栏的主题化是很复杂的(不同于Activity其他部分的主题化)。你可以得到一个上下文(Context),用这个上下文创建的自定义组件可以得到正确的主题。 ThumbnailUtils——帮助创建缩略图。通常我都是用现有的图片加载库(比如,Picasso 或者 Volley),不过这个ThumbnaiUtils可以创建视频缩略图。译者注:该API从V8才开始支持。 Context.getExternalFilesDir()———— 申请了SD卡写权限后,你可以在SD的任何地方写数据,把你的数据写在设计好的合适位置会更加有礼貌。这样数据可以及时被清理,也会有更好的用户体验。此外,Android 4.0 Kitkat中在这个文件夹下写数据是不需要权限的,每个用户有自己的独立的数据存储路径。译者注:该API从V8才开始支持。 SparseArray——Map的高效优化版本。推荐了解姐妹类SparseBooleanArray、SparseIntArray和SparseLongArray。 PackageManager.setComponentEnabledSetting()——可以用来启动或者禁用程序清单中的组件。对于关闭不需要的功能组件是非常赞的,比如关掉一个当前不用的广播接收器。 SQLiteDatabase.yieldIfContendedSafely()——让你暂时停止一个数据库事务, 这样你可以就不会占用太多的系统资源。 Environment.getExternalStoragePublicDirectory()——还是那句话,用户期望在SD卡上得到统一的用户体验。用这个方法可以获得在用户设备上放置指定类型文件(音乐、图片等)的正确目录。 View.generateViewId()——每次我都想要推荐动态生成控件的ID。需要注意的是,不要和已经存在的控件ID或者其他已经生成的控件ID重复。 ActivityManager.clearApplicationUserData()—— 一键清理你的app产生的用户数据,可能是做用户退出登录功能,有史以来最简单的方式了。 Context.createConfigurationContext() ——自定义你的配置环境信息。我通常会遇到这样的问题:强制让一部分显示在某个特定的环境下(倒不是我一直这样瞎整,说来话长,你很难理解)。用这个实现起来可以稍微简单一点。 ActivityOptions ——方便的定义两个Activity切换的动画。 使用ActivityOptionsCompat 可以很好解决旧版本的兼容问题。 AdapterViewFlipper.fyiWillBeAdvancedByHostKThx()——仅仅因为很好玩,没有其他原因。在整个安卓开源项目中(AOSP the Android ——pen Source Project Android开放源代码项目)中还有其他很有意思的东西(比如GRAVITY_DEATH_STAR_I)。不过,都不像这个这样,这个确实有用 ViewParent.requestDisallowInterceptTouchEvent() ——Android系统触摸事件机制大多时候能够默认处理,不过有时候你需要使用这个方法来剥夺父级控件的控制权(顺便说一下,如果你想对Android触摸机制了解更多,这个演讲会令你惊叹不已。) Activity.isChangingConfigurations ()——如果在 Activity 中 configuration 会经常改变的话,使用这个方法就可以不用手动做保存状态的工作了。 SearchRecentSuggestionsProvider——可以创建最近提示效果的 provider,是一个简单快速的方法。 ViewTreeObserver——这是一个很棒的工具。可以进入到 VIew 里面,并监控 View 结构的各种状态,通常我都用来做 View 的测量操作(自定义视图中经常用到)。 org.gradle.daemon=true——这句话可以帮助减少 Gradle 构建的时间,仅在命令行编译的时候用到,因为 Android Studio 已经这样使用了。 DatabaseUtils——一个包含各种数据库操作的使用工具。 android:weightSum (LinearLayout)——如果想使用 layout weights,但是却不想填充整个 LinearLayout 的话,就可以用 weightSum 来定义总的 weight 大小。 android:duplicateParentState (View)——此方法可以使得子 View 可以复制父 View 的状态。比如如果一个 ViewGroup 是可点击的,那么可以用这个方法在它被点击的时候让它的子 View 都改变状态。 android:clipChildren (ViewGroup)——如果此属性设置为不可用,那么 ViewGroup 的子 View 在绘制的时候会超出它的范围,在做动画的时候需要用到。 android:fillViewport (ScrollView)——在这片文章中有详细介绍文章链接,可以解决在 ScrollView 中当内容不足的时候填不满屏幕的问题。 android:tileMode (BitmapDrawable)——可以指定图片使用重复填充的模式。 android:enterFadeDuration/android:exitFadeDuration (Drawables)——此属性在 Drawable 具有多种状态的时候,可以定义它展示前的淡入淡出效果。 android:scaleType (ImageView)——定义在 ImageView 中怎么缩放/剪裁图片,一般用的比较多的是“centerCrop”和“centerInside”。 Merge——此标签可以在另一个布局文件中包含别的布局文件,而不用再新建一个 ViewGroup,对于自定义 ViewGroup 的时候也需要用到;可以通过载入一个带有标签的布局文件来自动定义它的子部件。 AtomicFile——通过使用备份文件进行文件的原子化操作。这个知识点之前我也写过,不过最好还是有出一个官方的版本比较好 UrlQuerySanitizer——使用这个工具可以方便对 URL 进行检查。 Fragment.setArguments——因为在构建 Fragment 的时候不能加参数,所以这是个很好的东西,可以在创建 Fragment 之前设置参数(即使在 configuration 改变的时候仍然会导致销毁/重建)。 DialogFragment.setShowsDialog ()—— 这是一个很巧妙的方式,DialogFragment 可以作为正常的 Fragment 显示!这里可以让 Fragment 承担双重任务。我通常在创建 Fragment 的时候把 onCreateView ()和 onCreateDialog ()都加上,就可以创建一个具有双重目的的 Fragment。 FragmentManager.enableDebugLogging ()——在需要观察 Fragment 状态的时候会有帮助。 LocalBroadcastManager——这个会比全局的 broadcast 更加安全,简单,快速。像 otto 这样的 Event buses 机制对你的应用场景更加有用。 PhoneNumberUtils.formatNumber ()——顾名思义,这是对数字进行格式化操作的时候用的。 Region.op()——我发现在对比两个渲染之前的区域的时候很实用,如果你有两条路径,那么怎么知道它们是不是会重叠呢?使用这个方法就可以做到。 Application.registerActivityLifecycleCallbacks——虽然缺少官方文档解释,不过我想它就是注册 Activity 的生命周期的一些回调方法(顾名思义),就是一个方便的工具。 versionNameSuffix——这个 gradle 设置可以让你在基于不同构建类型的 manifest 中修改版本名这个属性,例如,如果需要在在 debug 版本中以”-SNAPSHOT”结尾,那么就可以轻松的看出当前是 debug 版还是 release 版。 CursorJoiner——如果你是只使用一个数据库的话,使用 SQL 中的 join 就可以了,但是如果收到的数据是来自两个独立的 ContentProvider,那么 CursorJoiner 就很实用了。 Genymotion——一个非常快的 Android 模拟器,本人一直在用。 -nodpi——在没有特别定义的情况下,很多修饰符(-mdpi,-hdpi,-xdpi等等)都会默认自动缩放 assets/dimensions,有时候我们需要保持显示一致,这种情况下就可以使用 -nodpi。 BroadcastRecevier.setDebugUnregister ()——又一个方便的调试工具。 Activity.recreate ()——强制让 Activity 重建。 PackageManager.checkSignatures ()——如果同时安装了两个 app 的话,可以用这个方法检查。如果不进行签名检查的话,其他人可以轻易通过使用一样的包名来模仿你的 app。 DateUtils.formatDateTime() 用来进行区域格式化工作,输出格式化和本地化的时间或者日期。 AlarmManager.setInexactRepeating 通过闹铃分组的方式省电,即使你只调用了一个闹钟,这也是一个好的选择,(可以确保在使用完毕时自动调用 AlarmManager.cancel ()。原文说的比较抽象,这里详细说一下:setInexactRepeating指的是设置非准确闹钟,使用方法:alarmManager.setInexactRepeating(AlarmManager.RTC, startTime,intervalL, pendingIntent),非准确闹钟只能保证大致的时间间隔,但是不一定准确,可能出现设置间隔为30分钟,但是实际上一次间隔20分钟,另一次间隔40分钟。它的最大的好处是可以合并闹钟事件,比如间隔设置每30分钟一次,不唤醒休眠,在休眠8小时后已经积累了16个闹钟事件,而在手机被唤醒的时候,非准时闹钟可以把16个事件合并为一个, 所以这么看来,非准时闹钟一般来说比较节约能源。 Formatter.formatFileSize() 一个区域化的文件大小格式化工具。通俗来说就是把大小转换为MB,G,KB之类的字符串。 ActionBar.hide()/.show() 顾名思义,隐藏和显示ActionBar,可以优雅地在全屏和带Actionbar之间转换。 Linkify.addLinks() 在Text上添加链接。很实用。 StaticLayout 在自定义 View 中渲染文字的时候很实用。 Activity.onBackPressed() 很方便的管理back键的方法,有时候需要自己控制返回键的事件的时候,可以重写一下。比如加入 “点两下back键退出” 功能。 GestureDetector 用来监听和相应对应的手势事件,比如点击,长按,慢滑动,快滑动,用起来很简单,比你自己实现要方便许多。 DrawFilter 可以让你在不调用onDrew方法的情况下,操作canvas,比了个如,你可以在创建自定义 View 的时候设置一个 DrawFilter,给父 View 里面的所有 View 设置反别名。 ActivityManager.getMemoryClass() 告诉你你的机器还有多少内存,在计算缓存大小的时候会比较有用. ViewStub 它是一个初始化不做任何事情的 View,但是之后可以载入一个布局文件。在慢加载 View 中很适合做占位符。唯一的缺点就是不支持标签,所以如果你不太小心的话,可能会在视图结构中加入不需要的嵌套。 SystemClock.sleep() 这个方法在保证一定时间的 sleep 时很方便,通常我用来进行 debug 和模拟网络延时。 DisplayMetrics.density 这个方法你可以获取设备像素密度,大部分时候最好让系统来自动进行缩放资源之类的操作,但是有时候控制的效果会更好一些.(尤其是在自定义View的时候). Pair.create() 方便构建类和构造器的方法。 Activity.startActivities() 常用于在应用程序中间启动其他的Activity. TextUtils.isEmpty() 简单的工具类,用于检测是否为空 Html.fromHtml() 用于生成一个Html,参数可以是一个字符串.个人认为它不是很快,所以我不怎么经常去用.(我说不经常用它是为了重点突出这句话:请多手动构建 Spannable 来替换 Html.fromHtml),但是它对渲染从 web 上获取的文字还是很不错的。 TextView.setError() 在验证用户输入的时候很棒 Build.VERSION_CODES 这个标明了当前的版本号,在处理兼容性问题的时候经常会用到.点进去可以看到各个版本的不同特性 Log.getStackTraceString() 方便的日志类工具,方法Log.v()、Log.d()、Log.i()、Log.w()和Log.e()都是将信息打印到LogCat中,有时候需要将出错的信息插入到数据库或一个自定义的日志文件中,那么这种情况就需要将出错的信息以字符串的形式返回来,也就是使用static String getStackTraceString(Throwable tr)方法的时候. LayoutInflater.from() 顾名思义,用于Inflate一个layout,参数是layout的id.这个经常写Adapter的人会用的比较多. ViewConfiguration.getScaledTouchSlop() 使用 ViewConfiguration 中提供的值以保证所有触摸的交互都是统一的。这个方法获取的值表示:用户的手滑动这个距离后,才判定为正在进行滑动.当然这个值也可以自己来决定.但是为了一致性,还是使用标准的值较好. PhoneNumberUtils.convertKeypadLettersToDigits 顾名思义.将字母转换为数字,类似于T9输入法, Context.getCacheDir() 获取缓存数据文件夹的路径,很简单但是知道的人不多,这个路径通常在SD卡上(这里的SD卡指的是广义上的SD卡,包括外部存储和内部存储)Adnroid/data/您的应用程序包名/cache/ 下面.测试的时候,可以去这里面看是否缓存成功.缓存在这里的好处是:不用自己再去手动创建文件夹,不用担心用户把自己创建的文件夹删掉,在应用程序卸载的时候,这里会被清空,使用第三方的清理工具的时候,这里也会被清空. ArgbEvaluator 用于处理颜色的渐变。就像 Chris Banes 说的一样,这个类会进行很多自动装箱的操作,所以最好还是去掉它的逻辑自己去实现它。这个没用过,不明其所以然,回头再补充. ContextThemeWrapper 方便在运行的时候修改主题. Space space是Android 4.0中新增的一个控件,它实际上可以用来分隔不同的控件,其中形成一个空白的区域.这是一个轻量级的视图组件,它可以跳过Draw,对于需要占位符的任何场景来说都是很棒的。 ValueAnimator.reverse() 这个方法可以很顺利地取消正在运行的动画.我超喜欢.
开发Blog记录 清理收藏夹 太多了,来不及看了。 http://blog.sina.com.cn/s/blog_67d95f40010113ec.htmlhttp://segmentfault.com/a/1190000000394972http://a.code4app.com/android/ListViewAnimations/526dfc8d6803fa8a62000000http://hukai.me/http://androidweekly.cn/http://stackvoid.com/custom-view-android/http://www.codota.com/http://blueve.me/archives/tag/%E7%AE%97%E6%B3%95http://www.cyrilmottier.com/http://osgames.duapp.com/gamebuilder.php?appid=osgames1-961421749977376http://www.2cto.com/kf/201307/225694.htmlhttp://my.oschina.net/shaorongjie/blog/202820http://www.haoxiqiang.info/static/timing.htmlhttp://www.cnblogs.com/cpacm/p/3915302.html?utm_source=tuicoolhttp://lmbj.net/http://www.apkbus.com/android-231875-1-1.htmlhttp://www.jianshu.com/collection/06bbfc49e803http://blog.aaapei.com/http://lzyblog.com/http://blog.sina.com.cn/s/blog_67d95f40010113ec.htmlhttp://segmentfault.com/a/1190000000394972http://a.code4app.com/android/ListViewAnimations/526dfc8d6803fa8a62000000http://hukai.me/http://blog.csdn.net/ekeuy/article/details/42556865http://blog.csdn.net/lengguoxing/article/details/42126281http://blog.csdn.net/coder_pig/article/details/38977829http://blog.csdn.net/bboyfeiyu/article/details/39051521http://blog.csdn.net/fengyuzhengfan/article/details/38470675http://blog.csdn.net/carlin321/article/details/36480251http://blog.csdn.net/mingyue_1128/article/details/31376159http://blog.csdn.net/wangjinyu501/article/details/31386371http://blog.csdn.net/bboyfeiyu/article/details/38958829http://blog.csdn.net/bboyfeiyu/article/details/39719543http://blog.csdn.net/awangyunke/article/details/22047987http://blog.csdn.net/lmj623565791/article/details/38960443http://blog.csdn.net/lmj623565791/article/details/39102591http://blog.csdn.net/guolin_blog/article/details/9097463http://blog.csdn.net/guolin_blog/article/details/9153747
Android Design Support Library使用详解 Google在2015的IO大会上,给我们带来了更加详细的Material Design设计规范,同时,也给我们带来了全新的Android Design Support Library,在这个support库里面,Google给我们提供了更加规范的MD设计风格的控件。最重要的是,Android Design Support Library的兼容性更广,直接可以向下兼容到Android 2.2。这不得不说是一个良心之作。 使用Support Library非常简单: 添加引用即可: compile 'com.android.support:design:22.2.0' 下面我们来看看这些新控件的基本使用方法,我们从最简单的控件开始说起。 部分内容直接来自Android Developer Blog中的内容: 英文原文: http://android-developers.blogspot.jp/2015/05/android-design-support-library.html 菠萝的翻译: http://www.jcodecraeer.com/a/anzhuokaifa/developer/2015/0531/2958.html Snackbar Snackbar提供了一个介于Toast和AlertDialog之间轻量级控件,它可以很方便的提供消息的提示和动作反馈。 Snackbar的使用与Toast的使用基本相同: Snackbar.make(view, "Snackbar comes out", Snackbar.LENGTH_LONG) .setAction("Action", new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText( MainActivity.this, "Toast comes out", Toast.LENGTH_SHORT).show(); } }).show(); 需要注意的是,这里我们把第一个参数作为Snackbar显示的基准元素,而设置的Action也可以设置多个。 显示的效果就类似如下: Snackbar在出现一定时间后,就会消失,这与Toast一模一样。 Google API Doc 官方说明: http://developer.android.com/reference/android/support/design/widget/Snackbar.html TextInputLayout TextInputLayout作为一个父容器控件,包装了新的EditText。通常,单独的EditText会在用户输入第一个字母之后隐藏hint提示信息,但是现在你可以使用TextInputLayout 来将EditText封装起来,提示信息会变成一个显示在EditText之上的floating label,这样用户就始终知道他们现在输入的是什么。同时,如果给EditText增加监听,还可以给它增加更多的floating label。 下面我们来看这与一个TextInputLayout: <android.support.design.widget.TextInputLayout android:id="@+id/til_pwd" android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content"/> </android.support.design.widget.TextInputLayout> 一定要注意,他是把EditText包含起来的,不能单独使用。 在代码中,我们给它设置监听: final TextInputLayout textInputLayout = (TextInputLayout) findViewById(R.id.til_pwd); EditText editText = textInputLayout.getEditText(); textInputLayout.setHint("Password"); editText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { if (s.length() > 4) { textInputLayout.setError("Password error"); textInputLayout.setErrorEnabled(true); } else { textInputLayout.setErrorEnabled(false); } } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { } }); } 这样:显示效果如下: 当输入时: 这里需要注意的是,TextInputLayout的颜色来自style中的colorAccent的颜色: <item name="colorAccent">#1743b7</item> 下面我们给出Google API Doc上的说明,了解TextInputLayout的详细使用方法: http://developer.android.com/reference/android/support/design/widget/TextInputLayout.html Floating Action Button floating action button 是一个负责显示界面基本操作的圆形按钮。Design library中的FloatingActionButton 实现了一个默认颜色为主题中colorAccent的悬浮操作按钮,like this: FloatingActionButton——FAB使用非常简单,你可以指定在加强型FrameLayout里面——CoordinatorLayout,这个我们后面再将。 关于FAB的使用,你可以把它当做一个button即可。 <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:layout_margin="@dimen/fab_margin" android:src="@drawable/ic_done"/> 通过指定layout_gravity就可以指定它的位置。 同样,你可以通过指定anchor,即显示位置的锚点: <android.support.design.widget.FloatingActionButton android:layout_height="wrap_content" android:layout_width="wrap_content" app:layout_anchor="@id/app_bar" app:layout_anchorGravity="bottom|right|end" android:src="@android:drawable/ic_done" android:layout_margin="15dp" android:clickable="true"/> 除了一般大小的悬浮操作按钮,它还支持mini size(fabSize=”mini”)。FloatingActionButton继承自ImageView,你可以使用android:src或者ImageView的任意方法,比如setImageDrawable()来设置FloatingActionButton里面的图标。 http://developer.android.com/reference/android/support/design/widget/FloatingActionButton.html TabLayout Tab滑动切换View并不是一个新的概念,但是Google却是第一次在support库中提供了完整的支持,而且,Design library的TabLayout 既实现了固定的选项卡 - view的宽度平均分配,也实现了可滚动的选项卡 - view宽度不固定同时可以横向滚动。选项卡可以在程序中动态添加: TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.addTab(tabLayout.newTab().setText("tab1")); tabLayout.addTab(tabLayout.newTab().setText("tab2")); tabLayout.addTab(tabLayout.newTab().setText("tab3")); 但大部分时间我们都不会这样用,通常滑动布局都会和ViewPager配合起来使用,所以,我们需要ViewPager来帮忙: mViewPager = (ViewPager) findViewById(R.id.viewpager); // 设置ViewPager的数据等 setupViewPager(); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.setupWithViewPager(mViewPager); 通过一句话setupWithViewPager,我们就把ViewPager和TabLayout结合了起来。 http://developer.android.com/reference/android/support/design/widget/TabLayout.html NavigationView NavigationView在MD设计中非常重要,之前Google也提出了使用DrawerLayout来实现导航抽屉。这次,在support library中,Google提供了NavigationView来实现导航菜单界面,所以,新的导航界面可以这样写了: <android.support.v4.widget.DrawerLayout android:id="@+id/dl_main_drawer" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <!-- 你的内容布局--> <include layout="@layout/navigation_content"/> <android.support.design.widget.NavigationView android:id="@+id/nv_main_navigation" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/navigation_header" app:menu="@menu/drawer_view"/> </android.support.v4.widget.DrawerLayout> 其中最重要的就是这两个属性: app:headerLayout app:menu 通过这两个属性,我们可以非常方便的指定导航界面的头布局和菜单布局: 其中最上面的布局就是app:headerLayout所指定的头布局: <?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="200dp" android:background="?attr/colorPrimaryDark" android:gravity="center" android:orientation="vertical" android:padding="16dp" android:theme="@style/ThemeOverlay.AppCompat.Dark"> <ImageView android:layout_width="100dp" android:layout_height="100dp" android:layout_marginTop="16dp" android:background="@drawable/ic_user"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:gravity="center" android:text="XuYisheng" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textSize="20sp"/> </LinearLayout> 而下面的菜单布局,我们可以直接通过menu内容自动生成,而不需要我们来指定布局: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/nav_home" android:icon="@drawable/ic_dashboard" android:title="CC Talk"/> <item android:id="@+id/nav_messages" android:icon="@drawable/ic_event" android:title="HJ Class"/> <item android:id="@+id/nav_friends" android:icon="@drawable/ic_headset" android:title="Words"/> <item android:id="@+id/nav_discussion" android:icon="@drawable/ic_forum" android:title="Big HJ"/> </group> <item android:title="Version"> <menu> <item android:icon="@drawable/ic_dashboard" android:title="Android"/> <item android:icon="@drawable/ic_dashboard" android:title="iOS"/> </menu> </item> </menu> 你可以通过设置一个OnNavigationItemSelectedListener,使用其setNavigationItemSelectedListener()来获得元素被选中的回调事件。它为你提供被点击的 菜单元素 ,让你可以处理选择事件,改变复选框状态,加载新内容,关闭导航菜单,以及其他任何你想做的操作。例如这样: private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem menuItem) { menuItem.setChecked(true); mDrawerLayout.closeDrawers(); return true; } }); } 可见,Google将这些东西封装的非常易于使用了。 AppBarLayout AppBarLayout跟它的名字一样,把容器类的组件全部作为AppBar。like this: 这里就是把Toolbar和TabLayout放到了AppBarLayout中,让他们当做一个整体作为AppBar。 <android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> <android.support.design.widget.TabLayout android:id="@+id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content"/> </android.support.design.widget.AppBarLayout> http://developer.android.com/reference/android/support/design/widget/AppBarLayout.html CoordinatorLayout CoordinatorLayout是这次新添加的一个增强型的FrameLayout。在CoordinatorLayout中,我们可以在FrameLayout的基础上完成很多新的操作。 Floating View MD的一个新的特性就是增加了很多可悬浮的View,像我们前面说的Floating Action Button。我们可以把FAB放在任何地方,只需要通过: android:layout_gravity="end|bottom" 来指定显示的位置。同时,它还提供了layout_anchor来供你设置显示坐标的锚点: app:layout_anchor="@id/appbar" 创建滚动 CoordinatorLayout可以说是这次support library更新的重中之重。它从另一层面去控制子view之间触摸事件的布局,Design library中的很多控件都利用了它。 一个很好的例子就是当你将FloatingActionButton作为一个子View添加进CoordinatorLayout并且将CoordinatorLayout传递给 Snackbar.make(),在3.0及其以上的设备上,Snackbar不会显示在悬浮按钮的上面,而是FloatingActionButton利用CoordinatorLayout提供的回调方法,在Snackbar以动画效果进入的时候自动向上移动让出位置,并且在Snackbar动画地消失的时候回到原来的位置,不需要额外的代码。 官方的例子很好的说明了这一点: <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <! -- Your Scrollable View --> <android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.v7.widget.Toolbar ... app:layout_scrollFlags="scroll|enterAlways"> <android.support.design.widget.TabLayout ... app:layout_scrollFlags="scroll|enterAlways"> </android.support.design.widget.AppBarLayout> </android.support.design.widget.CoordinatorLayout> 其中,一个可以滚动的组件,例如RecyclerView、ListView(这里需要注意的是,貌似只支持RecyclerView、ListView,如果你用一个ScrollView,是没有效果的)。如果: 1、给这个可滚动组件设置了layout_behavior 2、给另一个控件设置了layout_scrollFlags 那么,当设置了layout_behavior的控件滑动时,就会触发设置了layout_scrollFlags的控件发生状态的改变。 设置的layout_scrollFlags有如下几种选项: scroll: 所有想滚动出屏幕的view都需要设置这个flag- 没有设置这个flag的view将被固定在屏幕顶部。 enterAlways: 这个flag让任意向下的滚动都会导致该view变为可见,启用快速“返回模式”。 enterAlwaysCollapsed: 当你的视图已经设置minHeight属性又使用此标志时,你的视图只能已最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。 exitUntilCollapsed: this flag causes the view to scroll off until it is ‘collapsed’ (its minHeight) before exiting。 需要注意的是,后面两种模式基本只有在CollapsingToolbarLayout才有用,而前面两种模式基本是需要一起使用的,也就是说,这些flag的使用场景,基本已经固定了。 例如我们前面例子中的,也就是这种模式: app:layout_scrollFlags="scroll|enterAlways" PS : 所有使用scroll flag的view都必须定义在没有使用scroll flag的view的前面,这样才能确保所有的view从顶部退出,留下固定的元素。 http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html CollapsingToolbarLayout CollapsingToolbarLayout提供了一个可以折叠的Toolbar,这也是Google+、photos中的效果。Google把它做成了一个标准控件,更加方便大家使用。 这里先看一个例子: <android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="@dimen/detail_backdrop_height" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:expandedTitleMarginEnd="64dp" app:expandedTitleMarginStart="48dp" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView android:id="@+id/backdrop" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:scaleType="centerCrop" android:src="@drawable/ic_banner" app:layout_collapseMode="parallax"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> 我们在CollapsingToolbarLayout中放置了一个ImageView和一个Toolbar。并把这个CollapsingToolbarLayout放到AppBarLayout中作为一个整体。在CollapsingToolbarLayout中,我们分别设置了ImageView和一个Toolbar的layout_collapseMode。 这里使用了CollapsingToolbarLayout的app:layout_collapseMode=”pin”来确保Toolbar在view折叠的时候仍然被固定在屏幕的顶部。当你让CollapsingToolbarLayout和Toolbar在一起使用的时候,title会在展开的时候自动变得大些,而在折叠的时候让字体过渡到默认值。必须注意,在这种情况下你必须在CollapsingToolbarLayout上调用setTitle(),而不是在Toolbar上。 除了固定住view,你还可以使用app:layout_collapseMode=”parallax”(以及使用app:layout_collapseParallaxMultiplier=”0.7”来设置视差因子)来实现视差滚动效果(比如CollapsingToolbarLayout里面的一个ImageView),这中情况和CollapsingToolbarLayout的app:contentScrim=”?attr/colorPrimary”属性一起配合更完美。 在这个例子中,我们同样设置了: app:layout_scrollFlags="scroll|exitUntilCollapsed"> 来接收一个: app:layout_behavior="@string/appbar_scrolling_view_behavior"> 这样才能产生滚动效果,而通过layout_collapseMode,我们就设置了滚动时内容的变化效果。 再来看一个官方的实例: CoordinatorLayout与自定义view 有一件事情必须注意,那就是CoordinatorLayout并不知道FloatingActionButton或者AppBarLayout的内部工作原理 - 它只是以Coordinator.Behavior的形式提供了额外的API,该API可以使子View更好的控制触摸事件与手势以及声明它们之间的依赖,并通过onDependentViewChanged()接收回调。 可以使用CoordinatorLayout.DefaultBehavior(你的View.Behavior.class)注解或者在布局中使用app:layout_behavior=”com.example.app.你的View$Behavior”属性来定义view的默认行为。framework让任意view和CoordinatorLayout结合在一起成为了可能。 http://developer.android.com/reference/android/support/design/widget/CollapsingToolbarLayout.html 总结 经过几天的研究,Google这次提出的Android Design Support Library的意义其实并不在于给出了这些非常好的控件,其实这些控件在Github上基本都能找到相应的。它的目的在于Google给出了官方的设计指导,进一步完善了MD设计思想。这才是Android Design Support Library最重要的特性。当然,平心而论,这些控件的使用并不是非常的人性化,过多的封装导致整个效果不是非常的具有可定制性,但是,这毕竟是Google迈出的第一步,后面一定会更加牛逼。 Demo 最后,给出一个融合MD和Android Design Support Library的Demo供大家研究,相信结合文章和代码,大家一定能很快理解Android Design Support Library的使用方法。 DesignSupportLibraryDemo https://github.com/xuyisheng/DesignSupportLibraryDemo 欢迎大家star、fork。 当前版本还未完善,很多画面还在处理中。后续会进一步丰富、完善,作为一个MD设计的Demo。
Android Studio使用jni、so库 在Android Studio1.1之后,AS就已经支持jni和so库了,马上发布的1.3正式版,更是可以在clion环境下编译c、c++,更加方便的使用NDK进行开发,网上有很多讲在Android Studio中使用jni的方法,但大多都是在1.1之前的,那时候还没有直接支持jni,所以需要通过给gradle增加task的方式来添加支持。而现在,这一切都不是事!!! 添加lib库 切换到project标签,直接将jar包复制到libs目录下,在添加dependency就可以了。 添加so库 添加so库有两个方法。但是强烈建议使用简单粗暴的方式。 简单粗暴 在main目录点击右键,添加文件夹,命名为jniLibs,需要注意的是,一个字都不能错,这是默认名。 自定义目录 在main下面你可以自定义一个目录,例如:xys。然后在build.gradle的Android标签下,添加如下修改: sourceSets { main { jniLibs.srcDirs = ['xys'] } } 指定下jniLibs的具体目录即可。 使用 在代码中,只需要: static { System.loadLibrary("XXXX"); } 并且指定native对应的方法: public static native void nativeXXXXX(); 这些应该不用讲了。 警告 在使用jni的时候,有几个地方非常需要注意。 包名 在loadLibrary的那个程序,包名一定要和so库的包名一样。 so库版本 在jniLibs下,最好放置: 这样几个版本最好都要,但是实际上,放一个armeabi-v7a就够了。但是出错的时候,一定要往这个地方上去想。 为什么只用armeabi-v7a,这也是目前大多数Android 产品这样做法; 1.armeabi-v7a支持浮点操作,所以速度更快,并且目前绝大多数机器支持armeabi-v7a;(来自arm中国team的朋友以及业内信息) 2.armeabi 针对普通的或旧的arm v5 cpu,但是速度慢,得不到最新设备的CPU兼容优势(高级扩展功能,浮点运算); 3. x86 机子太少,没必要做兼容;(除非我们完全不care包size)
创建兼容Android Studio和eclipse的AS工程 虽然我的博客名叫eclipse_xu,但是我已经将近一年多没有用过eclipse了,早已拜在Android Studio门下。但是,最近的项目由于要兼容eclipse和Android Studio,让一些还未脱贫的朋友也能使用AS创建的工程,所以,找到了一种能够同时兼容ant和gradle的方式,来创建兼容的工程。 创建普通的Android Studio工程 非常简单,创建好之后,我们切换到project标签,目录结构是这样的: 但是eclipse的目录结构不是这样的,所以eclipse默认的ant就无法编译这样的工程,所以我们需要对目录进行下修改: 删除main文件夹,将java文件夹内的代码移动到src中,作为代码文件夹。 如下图所示: 但是你这样改了,gradle又不认了,所以,在这生死存亡之际,我们再取修改下build.gradle文件。 在Android标签下,增加如下所示的配置: sourceSets { main { java.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] jni.srcDirs = ['jni'] jniLibs.srcDirs = ['libs'] manifest.srcFile 'AndroidManifest.xml' } } 相信大家都能看的懂,其实就是重新制定下对应的文件夹,例如src、res等。 这样,我们再切换到Android标签下,显示的结构其实和原来是一样的。但是这样的工程却可以作为lib库给eclipse工程直接引用。 外传:导入eclipse项目 除了使用eclipse导出gradle项目的方式来导入Android Studio。我们也可以直接打开eclipse工程,即直接open eclipse project。但是,最重要的是,导入之后,直接在项目配置中删除这个module,重新import module,再次选择我们刚刚导入的项目,这时候,AS就会提示你使用gradle来编译项目了。这样也同样完美的兼容了eclipse和Android Studio。 警告 虽然本文讲解了如何兼容Android Studio和eclipse项目的方法,但坚决反对继续使用eclipse进行Android App开发,你看看2015 Google IO 上,Android Studio已经拉开eclipse几个天文单位了,不使用工具革新生产力,我只能说%¥%#……&&(……¥……¥#*&。
转自:http://support.google.com/chrome/bin/answer.py?hl=zh-Hans&answer=165450 标签页和窗口快捷键 ⌘-N 打开新窗口。 ⌘-T 打开新标签页。 ⌘-Shift-N 在隐身模式下打开新窗口。 按 ⌘-O,然后选择文件。 在 Google Chrome 浏览器中打开计算机中的文件。 按住 ⌘ 键,然后点击链接。或用鼠标中键(或鼠标滚轮)点击链接。 从后台在新标签页中打开链接。 按住 ⌘-Shift 键,然后点击链接。或按住 Shift 的同时用鼠标中键(或鼠标滚轮)点击链接。 在新标签页中打开链接并切换到刚打开的标签页。 按住 Shift 键,然后点击链接。 在新窗口中打开链接。 ⌘-Shift-T 重新打开上次关闭的标签页。Google Chrome 浏览器可记住最近关闭的 10 个标签页。 将标签页拖出标签栏。 在新窗口中打开标签页。 将标签页从标签栏拖到现有窗口中。 在现有窗口中打开标签页。 同时按 ⌘-Option 和向右箭头键。 切换到下一个标签页。 同时按 ⌘-Option 和向左箭头键。 切换到上一个标签页。 ⌘-W 关闭当前标签页或弹出窗口。 ⌘-Shift-W 关闭当前窗口。 点击并按住浏览器工具栏中的后退或前进箭头。 在新标签页中显示浏览历史记录。 按 Delete 或 ⌘-[ 转到当前标签页的上一页浏览历史记录。 按 Shift-Delete 或 ⌘-] 转到当前标签页的下一页浏览历史记录。 按住 Shift,然后点击窗口左上方的 + 按钮。 最大化窗口。 ⌘-M 最小化窗口。 ⌘-H 隐藏 Chrome 浏览器。 ⌘-Option-H 隐藏其他所有窗口。 ⌘-Q 关闭 Google Chrome 浏览器。 Google Chrome 浏览器功能快捷键 ⌘-Shift-B 打开和关闭书签栏。 ⌘-Option-B 打开书签管理器。 ⌘-, 打开“偏好设置”对话框。 ⌘-Y 打开“历史记录”页面。 ⌘-Shift-J 打开“下载内容”页面。 ⌘-Shift-Delete 打开“清除浏览数据”对话框。 ⌘-Shift-M 在多个用户之间切换。 地址栏快捷键 在地址栏中可使用以下快捷键: 输入搜索字词,然后按 Enter。 使用默认搜索引擎进行搜索。 输入搜索引擎关键字,按空格键,然后输入搜索字词,再按 Enter。 使用与关键字相关联的搜索引擎进行搜索。 首先输入搜索引擎网址,然后在系统提示时按 Tab,输入搜索字词,再按 Enter。 使用与网址相关联的搜索引擎进行搜索。 输入网址,然后按 ⌘-Enter。 在新后台标签页中打开网址。 ⌘-L 突出显示网址。 ⌘-Option-F 将“?”置于地址栏中。在问号后输入搜索字词可用默认搜索引擎执行搜索。 同时按 Option 和向左箭头键。 将光标移到地址栏中的前一个关键字词 同时按 Option 和向右箭头键。 在地址栏中将光标移到下一个关键字词 同时按 Shift-Option 和向左箭头键。 在地址栏中突出显示上一关键字词 同时按 Shift-Option 和向右箭头键。 在地址栏中突出显示下一关键字词 ⌘-Delete 在地址栏中删除光标前的关键字词 用键盘上的方向键从地址栏下拉菜单中选择一个条目,然后按 Shift-Fn-Delete。 从浏览历史记录中删除所选条目(如果可以)。 在地址栏菜单中按 Page Up 或 Page Down。 在菜单中选择上一条目或下一条目。 网页快捷键 ⌘-P 打印当前网页。 ⌘-Shift-P 打开“网页设置”对话框。 ⌘-S 保存当前网页。 ⌘-Shift-I 通过电子邮件发送当前网页。 ⌘-R 重新加载当前网页。 ⌘-, 停止加载当前网页。 ⌘-F 打开查找栏。 ⌘-G 在查找栏中查找下一条与输入内容相匹配的内容。 ⌘-Shift-G 或 Shift-Enter 在查找栏中查找上一条与输入内容相匹配的内容。 ⌘-E 使用所选内容查找 ⌘-J 跳到所选内容 ⌘-Option-I 打开开发者工具。 ⌘-Option-J 打开“JavaScript 控制台”。 ⌘-Option-U 打开当前网页的源代码。 按住 Option,然后点击链接。 下载链接目标。 将链接拖到书签栏中。 将链接保存为书签。 ⌘-D 将当前网页保存为书签。 ⌘-Shift-D 将所有打开的标签页以书签的形式保存在新文件夹中。 ⌘-Shift-F 在全屏模式下打开网页。再按一次 ⌘-Shift-F 可退出全屏模式。 ⌘-+ 放大网页上的所有内容。 ⌘ 和 - 缩小网页上的所有内容。 ⌘-0 将网页上的所有内容都恢复到正常大小。 ⌘-Shift-H 在当前标签页中打开主页。 空格键 向下滚动网页。 ⌘-Option-F 搜索网页。 文本快捷键 ⌘-C 将突出显示的内容复制到剪贴板中。 ⌘-Option-C 将您正在查看的网页的网址复制到剪贴板中。 ⌘-V 从剪贴板中粘贴内容。 ⌘-Shift-Option-V 仅粘贴内容,不带源格式。 ⌘-X 或 Shift-Delete 删除突出显示的内容并将其复制到剪贴板中。 ⌘-Z 撤消最后一步操作。 ⌘-Shift-Z 重复最后一步操作。 ⌘-X 删除突出显示的内容并将其保存到剪贴板中(剪切)。 ⌘-A 选择当前网页上的所有文本。 ⌘-: 打开“拼写和语法”对话框。 ⌘-; 检查当前网页上的拼写和语法
玩转CSDN之自定义博客栏目 不得不说,CSDN在IT界还是非常不错的, 不管是文章数量还是质量,都非常不错,很多程序猿也在CSDN建了窝,那么如何把CSDN的主页设置的更加符合自己的口味,就是我们今天要做的事。 CSDN博客的栏目指的是这块内容: 这里面,有的是CSDN博客自带的内容,比如个人资料、博客专栏等,还有些内容,我们可以自定义,首先,我们需要进入个人的博客首页,点击管理博客,并切换到博客栏目选项卡,这里,就是我们修改自定义栏目的主战场了。 自定义链接 我们首先来看最简单的,增加一个栏目,并增加一些自定义的链接选项。 首先,我们点击添加栏目,如图: 标题就是我们自定义栏目的名字,随便取一个看的顺眼的名字即可。 下面的内容,才是我们的重点,白话文我们就不说了,你可以在内容中输入类似“公告”、“声明”、“通缉”、“悬赏”等等,不用任何修饰的白话文,这些东西, 相信小学毕业证书拿到的朋友应该都会。 那么如何输入带链接的内容呢?几个大字看见没!支持HTML格式!!!有了这几个字,还怕我们有什么做不出吗? OK。上链接: <a title="友情链接" href="http://www.hztalk.com/" target="_blank"><br> 聊科技 游戏 电影 美食 请访问 HZtalk </a> <br> 相信这些最基本的HTML语言,大部分开发者都应该看得懂,看不懂就不用继续往下看了。 自定义带框框的链接 同样是一个链接,如下图的这个标题样式,是不是显得略高级点呢? 如果不写标题,那么默认就是一行文字,所以我们给它增加一个系统的栏目才有的标题框 注意,是红色框框里面的内容,不是红色的框框。。。 <a title="友情链接" href="http://www.hztalk.com/" target="_blank"><br> 聊科技 游戏 电影 美食 请访问 HZtalk </a> <br> <a title="Github" href="https://github.com/xuyisheng" target="_blank"><br> <ul class="panel_head"><span>我的Github</span></ul><br> 欢迎Follow、Fork、Star </a> 代码中把前面的内容代码一起贴了出来,让大家把结构更看的清楚一点。我们只是加了一个 <ul class="panel_head"> 而已。 贴图 高大上的边栏怎么能没有图片,可惜的是,CSDN不允许引用站外图片。。。 所以,我们只能寄希望于CSDN自己的相册中的图片,但是。。。CSDN的相册隐藏的如此之好,以至于我们只能通过源代码来找到它的位置。。。所以,这里还是直接告诉大家吧: 点击进入我的CSDN首页——把鼠标放到我的收藏旁边的下拉箭头上——我的相册出来了,不知道这是怎么设计出来的,太反人类了。当我们把图片上传到相册中后,就可以使用相册中的图片了。选中图片,右键选择,在新窗口中打开图片,就获得了图片的地址,有了站内的图片,引用就非常简单了。 <ul class="panel_head"><span>我的微信公众号</span></ul> <ul class="panel_body"> 为你推荐最新的博文~更有惊喜等着你 <img style="width:95%;" src="http://img.my.csdn.net/uploads/201503/15/1426428496_7596.jpg"> </ul> 效果就是这样: 如果你想居中的话: <center><img src='imgurl'></center> 其实这些都是最基本的HTML语句,我这样的半吊子Web开发者都能写。相信大半吊子的程序猿应该可以用HTML写出更好的内容。 Flash 添加Flash与使用图片几乎没有太大差别: 例如我们要显示凯子哥页面上的这个动画效果: 我们可以这样: <div id="custom_column_28798789" class="panel"> <ul class="panel_head"><span>个人说明</span></ul> <ul class="panel_body"> <div height="120" width="150" align="center"><embed height="120" width="150" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" src="http://chabudai.sakura.ne.jp/blogparts/honehoneclock/honehone_clock_wh.swf" quality="high" autostart="1" wmode="transparent"></div> 还是那句话,都是HTML。 新浪微博 新浪微博也是程序员交(zhuang)流(bi)的好东西,所以,这里我们再来配置下新浪微博,但是,这个东西我们不好直接用HTML来做,毕竟写上去毕竟难看,不信邪你可以试试。 那么我们怎么做呢?首先,我们需要帮助。。。 打开微博工具,直接给地址:戳我戳我 上面都是广告,我们要的是下面。 看见没,多么贴心,还给我们准备好了复制。 我们把这个代码直接复制到栏目中: <iframe width="100%" height="550" class="share_self" frameborder="0" scrolling="no" src="http://widget.weibo.com/weiboshow/index.php?language=&amp;width=0&amp;height=550&amp;fansRow=2&amp;ptype=1&amp;speed=0&amp;skin=1&amp;isTitle=1&amp;noborder=1&amp;isWeibo=1&amp;isFans=1&amp;uid=1904977584&amp;verifier=05864e99&amp;dpc=1"></iframe> OK,微博已经自动获取了,不信可以贴上上面的代码,给我宣传宣传,thanks。 是不是so easy: 邮我 像QQ邮箱这样的鹅厂产品,大多都会带有一些社交类的元素,我们同样可以在页面中设置这样的信息,比如给我发邮件: 点击后,会跳到这样一个界面: 点击开发平台,你也可以使用这样的功能,跟使用微博一样,我们可以让它自动生成我们需要的样式,一键获取代码: 真的是so easy。 当然要注意,代码中会带有站外的图片,你同样需要传到自己的CSDN相册。 HTML代码示例 除了我们上面列举的这些常用的设置,下面我们再从网上拔一些常用的代码来,供大家参考: 贴图:&lt;img src=&quot;图片地址&quot;&gt; 加入连接:&lt;a href=&quot;所要连接的相关地址&quot;&gt;写上你想写的字&lt;/a&gt; 贴图:<img src="图片地址"> 加入连接:<a href="所要连接的相关地址">写上你想写的字</a> 在新窗口打开连接:<a href="相关地址" target="_blank">写上要写的字</a> 消除连接的下划线在新窗口打开连接: <a href="相关地址" style="text-decoration:none" target="_blank">写上你想写的字</a> 移动字体(走马灯):<marquee>写上你想写的字</marquee> 字体加粗:<b>写上你想写的字</b> 字体斜体:<i>写上你想写的字</i> 字体下划线: <u>写上你想写的字</u> 字体删除线: <s>写上你想写的字</s> 字体加大: <big>写上你想写的字</big> 字体控制大小:<h1>写上你想写的字</h1> (其中字体大小可从h1-h5,h1最大,h5最小) 更改字体颜色:<font color="#value">写上你想写的字</font>(其中value值在000000与ffffff(16位进制)之间 消除连接的下划线:<a href="相关地址" style="text-decoration:none">写上你想写的字</a> 贴音乐:<embed src=音乐地址 width=300 height=45 type=audio/mpeg autostart="false"> 贴flash: <embed src="flash地址" width="宽度" height="高度"> 贴影视文件:<img dynsrc="文件地址" width="宽度" height="高度" start=mouseover> 换行:<br> 段落:<p>段落</p> 原始文字样式:<pre>正文</pre> 换帖子背景:<body background="背景图片地址"> 固定帖子背景不随滚动条滚动:<body background="背景图片地址" body bgproperties=fixed> 定制帖子背景颜色:<body bgcolor="#value">(value值见10) 帖子背景音乐:<bgsound="背景音乐地址" loop=infinite> 贴网页:<iframe src="相关地址" width="宽度" height="高度"></iframe> 以上,基本可以玩转了。 可以看出,自定义CSDN的博客栏目,无非就是HTML!!!所以,包括但不限于上面的HTML,都可以设置我们的博客栏目。OK,点到即止,赶紧去自定义吧。
解放程序猿宝贵的右手(或者是左手) ——Android自动化测试技巧 Google大神镇楼 : http://developer.android.com/tools/testing-support-library/index.html#UIAutomator 前言: 觉得文章太长不想往后翻的朋友,你们会后悔的,当然,你也可以选择先看后面的,你会觉得很爽,但是相信我,你还是会回来看前面的。那么,还是慢慢往后翻吧。 导入: 人们懒的走路,才创造了汽车; 人们懒的爬楼,才创造了电梯; 人们懒的扫地,才创造了自动扫地机器人。 人类的进步,离不开这些喜欢偷懒的人,现在,程序猿将偷懒上升到了一个新的高度——利用程序来进行自动化软件测试,将测试工程师从繁琐的测试用例中解脱出来,从此可以一边喝着咖啡,一边看着程序自动测试,不必看着测试用例重复无数次的测试步骤,也不必担心操作失误而导致不必要的错误,更不用担心压力测试而导致的身心俱疲。想了解程序猿是如何实现自动化测试的吗,这里有你想要的答案。 声明 转载真的请注明出处: http://blog.csdn.net/eclipsexys 顺便打个广告: 我的慕课网视频: http://www.imooc.com/space/teacher/id/347333 为啥要测试 发现错误、为程序员提供修改意见 验证软件是否满足设计需求和技术需求 验证生产环境下真实的用户使用过程,分析用户体验 ——总而言之一句话——软件测试,决定着软件的质量。 以前在TCL的时候,每个软件版本都要不停的跑MonkeyTest,一个是检测系统ROM的稳定性,一个是检测各种第三方应用在ROM上的使用情况,所以经常会报出很多Monkey跑出来的Bug,这些Bug经过我们分析,会初步判断是第三方App的问题还是系统的ROM问题,如果是第三方的问题,我们也会提交给App的运营商,但是大部分的运营商给我们的回复都是,我们的App不支持跑Monkey,其实Monkey可以发现一些潜在的问题,特别是一些很难复现的问题,我以前的leader曾经说过一句话我觉得非常好,没有什么bug是不能复现的,没有复现,只是没有找到必先的步骤,所以每一个bug都不是偶然的,我们应该尽量严谨的分析每一个可能存在的bug。 再以前的时候,对日的公司对测试更是无比看重,各种UT测试式样书,不仅仅是要写好怎么测试、测试什么,而且测试的数据、中间过程还要截图,保留证据。 有哪些测试 Google CTS测试:兼容性测试,测试ROM的兼容性标准 Google GTS测试 实验室机器人测试、机械臂自动化模拟测试 Monkey Test压力测试 End User终端用户测试 对于美国的手机运营商,例如T-Mobile、Sprite、AT&T,他们都有一系列的手机性能测试,他们的测试项目、测试方法、测试过程,其实都是他们的商业机密,一个是保证测试结果的严谨性,一个也保证了手机厂商能够不作弊的完成测试,所以,千万不要学华X手机,在T-Mobile实验室偷拍手机测试机器人的软件、技术参数及其他机密信息,而被T-Mobile列入北美黑名单。逗比新闻 Android自动化测试工具 自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程 将大量重复的测试步骤用脚本代替,让机器完成重复工作 规范测试用例,保证测试质量 高——大——上 自动化测试的工具 MonkeyRunner monkeyrunner工具提供一个API来控制Android设备。可以写一个python脚本来安装应用,运行应用,发送键值,截图。monkeyrunner对python进行了封装,加入了一些针对Android设备的类。可以完全用python脚本来实现这些功能。 Instrumentation 基于Android单个Activitiy的测试框架。 Robotium 一个优秀的测试框架,基于Instrumentation的二次封装。 QTP 一个Web上的自动化测试工具,通过录制脚本来实现自动化测试。 UiAutomator 目前最佳的UI自动化测试框架。基于Android 4.X+系统,专业UI自动化测试,可以模拟用户对手机的各种行为。编写快速、可以使用大部分的Android API、无需签名,无任何Activity限制。 各个测试框架的优缺点如下表所示: 测试框架 使用语言 运行方式 限制 适用环境 MonkeyRunner Python ADB、Python 测试靠坐标 压力测试 Instrumentation Java ADB 只能单个Activity测试,且需要应用相同签名,代码量大 白盒测试 Robotium 同上 同上 同上 同上 UiAutomator Java ADB或者脱机 Android 4.X+ UI测试 综上所述,我们使用UiAutomator作为我们Android自动化测试的首选框架。 UiAutomator环境搭建 开发环境:eclipse(非常抱歉,还没学会如何使用AS来开发Java代码、进行jar打包,请了解的朋友留言!!!) 编译环境:Ant、Java、Android SDK UiAutomator基本对象之UiDevice 通常用于获取系统的设备信息、系统按键、全局操作等。 获取坐标参数 返回值 方法 解释 boolean click(int x, int y) 在点(x, y)点击 int getDisplayHeight() 获取屏幕高度 int getDisplayWidth() 获取屏幕宽度 Point getDisplaySizeDp() 获取显示尺寸大小 系统信息 返回值 方法 解释 void getCurrentPackageName() 获取当前界面包名 void getCurrentActivityName() 获取当前界面Activity void dumpWindowHierarchy(fileName) dump当前布局文件到/data/local/tmp/目录 滑动、拖拽 返回值 方法 解释 boolean drag(startX, startY, endX, endY, steps) 拖拽坐标处对象到另一个坐标 boolean swipe(segments, segmentSteps) 在Points[]中以segmentSteps滑动 boolean swipe(startX, startY, endX, endY, steps) 通过坐标滑动 系统按键 返回值 方法 解释 void wakeUp() 按电源键亮屏 void sleep() 按电源键灭屏 boolean isScreenOn() 亮屏状态 void setOrientationLeft() 禁用传感器,并左旋屏幕,固定 void setOrientationNatural() 禁用传感器,恢复默认屏幕方向,固定 void setOrientationRight() 禁用传感器,并右旋屏幕,固定 void unfreezeRotation() 启用传感器,并允许旋转 boolean isNaturalOrientation() 检测是否处于默认旋转状态 void getDisplayRotation() 返回当前旋转状态,0、1、、2、3分别代表0、90、180、270度旋转 void freezeRotation() 禁用传感器,并冻结当前状态 boolean takeScreenshot(storePath) 当前窗口截图、1.0f缩放、90%质量保存在storePath void takeScreenshot(storePath, scale, quality) 同上,但指定缩放和压缩比率 void openNotification() 打开通知栏 void openQuickSettings() 打开快速设置 等待窗口 返回值 方法 解释 void waitForIdle() 等待当前窗口处于空闲状态、默认10s void waitForIdle(long timeout) 自定义超时等待当前窗口处于空闲状态 boolean waitForWindowUpdate(packageName, timeout) 等待窗口内容更新 示例代码 // 输入按键 UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_A); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_B); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_C); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_A,1); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_B,1); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_C,1); // 点击 UiDevice.getInstance().click(400, 400); int h=UiDevice.getInstance().getDisplayHeight(); int w=UiDevice.getInstance().getDisplayWidth(); UiDevice.getInstance().click(w/2, h/2); // Swipe、Drag int startX, startY, endX, endY, steps; startX=300; startY=400; endX=startX; endY=startY + 200; steps=100; UiDevice.getInstance().drag(startX, startY, endX, endY, steps); int h=UiDevice.getInstance().getDisplayHeight(); int w=UiDevice.getInstance().getDisplayWidth(); UiDevice.getInstance().swipe(w, h/2, 30, h/2, 10); Point p1=new Point(); Point p2=new Point(); Point p3=new Point(); Point p4=new Point(); p1.x=250;p1.y=300; p2.x=600;p2.y=350; p3.x=800;p3.y=800; p4.x=200;p4.y=900; Point[] pp={p1,p2,p3,p4}; UiDevice.getInstance().swipe(pp, 50); // 灭屏、亮屏 UiDevice.getInstance().sleep(); UiDevice.getInstance().wakeUp(); // Notification UiDevice.getInstance().openNotification(); sleep(3000); UiDevice.getInstance().openQuickSettings(); UiDevice.getInstance().dumpWindowHierarchy("ui.xml"); 送个视频,让大家真实体验下: 视频代码: UiDevice.getInstance().pressBack(); UiDevice.getInstance().pressBack(); UiDevice.getInstance().pressHome(); sleep(1000); UiDevice.getInstance().pressMenu(); sleep(1000); UiDevice.getInstance().pressBack(); sleep(1000); UiDevice.getInstance().pressRecentApps(); sleep(1000); UiDevice.getInstance().pressHome(); sleep(1000); UiDevice.getInstance().click(240, 1100); sleep(2000); UiDevice.getInstance().click(670, 1100); sleep(2000); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_H); sleep(1000); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_H, 1); sleep(1000); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_J); sleep(1000); UiDevice.getInstance().pressKeyCode(KeyEvent.KEYCODE_J, 1); sleep(1000); UiDevice.getInstance().swipe(30, 400, 600, 400, 10); sleep(1000); UiDevice.getInstance().pressHome(); sleep(1000); UiDevice.getInstance().drag(660, 860, 360, 360, 50); sleep(1000); UiDevice.getInstance().sleep(); sleep(1000); UiDevice.getInstance().wakeUp(); sleep(1000); UiDevice.getInstance().swipe(370, 1000, 370, 200, 50); sleep(1000); UiDevice.getInstance().takeScreenshot(new File("/sdcard/uidevice.png")); UiAutomator基本对象之UiSelector 通常使用UiSelector,通过各种属性节点和关系来定位组件,类似SQL语句的where条件。 uiautomatorviewer 要查看界面UI元素的层级关系,我们需要使用SDK/tools/下面的uiautomatorviewer工具来帮助我们进行查看,运行uiautomatorviewer,点击dump,我们就可以获取当前界面的UI快照。 下面这张图就是一个示例: 通过uiautomatorviewer,我们可以找到很多对象的属性,上图右下角的方框中的,都是对象所具有的属性。我们可以通过这些属性来定位需要的元素对象,这里要注意的是,uiautomator可以使用链式查找,即一个条件无法定位,那么可以通过多个条件组合,来定位一个元素。 通过text、description属性定位 返回值 方法 解释 UiSelector text(text) 通过text完全定位 UiSelector textContains(text) 通过text包含定位 UiSelector textMatches(regex) 通过text正则定位 UiSelector textStartsWith(text) 通过text起始文字定位 UiSelector description(text) 通过text完全定位 UiSelector descriptionContains(text) 通过description包含定位 UiSelector descriptionMatches(regex) 通过description正则定位 UiSelector descriptionStartsWith(text) 通过description起始文字定位 通过resourceId定位 返回值 方法 解释 UiSelector resourceId(id) 通过resourceId定位 UiSelector resourceIdMatches(regex) 通过resourceId正则定位 通过class、package定位 这种方式适用于当前页面上只有一种类型的组件的情况,例如只有一个ListView。 返回值 方法 解释 UiSelector className(className) 通过class定位 UiSelector classNameMatches(regex) 通过class正则定位 UiSelector packageName(name) 通过package定位 UiSelector packageNameMatches(regex) 通过package正则定位 通过index、instance定位 返回值 方法 解释 UiSelector index(index) 通过index定位 UiSelector instance(instance) 通过instance定位 通过其它属性定位 返回值 方法 解释 UiSelector enabled(val) 通过enabled属性定位 …… …… …… 对象的所有属性都可以使用,这里不再列举。 示例代码 // 找到对象 点击对象 UiSelector l=new UiSelector().text("联系人"); UiObject object=new UiObject(l); object.click(); // 匹配方式 // 完全匹配:联系人 // 包含匹配:系人 // 正则匹配:.*系.* // 起始文字匹配:联系 UiSelector l=new UiSelector().textContains("系人"); UiSelector l=new UiSelector().textMatches(".*系.*"); UiSelector l=new UiSelector().textStartsWith("联系"); UiObject object=new UiObject(l); object.click(); UiAutomator基本对象之UIObject UIObject是UiAutomator的核心属性之一。它代表了整个UI界面中的所有对象元素。 它的功能包括:获取UI元素,点击、拖拽、滑动、对象属性判断、手势等。 点击与长按 返回值 方法 解释 boolean click() 点击对象 boolean clickAndWaitForNewWindow() 点击对象并等待新窗口出现 boolean clickAndWaitForNewWindow(timeout) 点击对象并等待新窗口出现,指定延迟 boolean clickBottomRight() 点击对象右下角 boolean clickTopLeft() 点击对象左上角 boolean longClick() 长按对象 boolean longClickBottomRight() 点击对象右下角 boolean longClickTopLeft() 点击对象左上角 拖拽与滑动 返回值 方法 解释 boolean dragTo(destObj, steps) 以steps拖动对象到destObj boolean dragTo(destX, destY, steps) 以steps拖动对象到坐标 boolean swipeDown(steps) 向下拖动 boolean swipeLeft(steps) 向左拖动 boolean swipeRight(steps) 向右拖动 boolean swipeTop(steps) 向上拖动 文本输入与清除 返回值 方法 解释 boolean setText(text) 设置内容为text boolean clearTextField() 清除文本 获取对象属性 返回值 方法 解释 Rect getBounds() 获取对象矩形范围 int getChildCount() 获取子View数量 …… …… …… 还有很多,不列举了。 获取对象属性状态 返回值 方法 解释 boolean isCheckable() 获取对象checkable状态 …… …… …… 还有很多,不列举了。 获取对象存在状态 返回值 方法 解释 boolean waitForExists(timeout) 等待对象出现 boolean waitUntilGone(timeout) 等待对象消失 boolean exists() 对象是否存在 手势状态 返回值 方法 解释 boolean performMultiPointerGesture(touches) 执行单指手势 boolean performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps) 执行双指手势 boolean pinchIn(percent, steps) 双指向内收缩 boolean pinchOut(percent, steps) 双指向外张开 示例代码 // 拖拽 UiObject object1=new UiObject(new UiSelector().text("联系人")); UiObject object2=new UiObject(new UiSelector().text("图库")); object1.dragTo(300,1200, 10); object1.dragTo(object2, 30); object1.swipeUp(5); // 输入、清空 UiObject edit=new UiObject(new UiSelector() .resourceId("com.hjwordgames:id/edit_password")); edit.setText("xuyisheng"); sleep(2000); edit.clearTextField(); // 判断 UiObject wlan=new UiObject(new UiSelector() .resourceId("com.android.settings:id/switchWidget")); if(!wlan.isChecked()){ wlan.click(); } // 手势 UiObject object=new UiObject(new UiSelector() .resourceId("com.android.gallery3d:id/photopage_bottom_controls")); object.pinchIn(80, 20); object.pinchOut(80, 20); Point startPoint1, startPoint2, endPoint1, endPoint2; startPoint1=new Point(); startPoint2=new Point(); endPoint1=new Point(); endPoint2=new Point(); startPoint1.x=150;startPoint1.y=200; startPoint2.x=100;startPoint2.y=500; endPoint1.x=900;endPoint1.y=200; endPoint2.x=950;endPoint2.y=500; object.performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, 50); 再送一个视频、不收费: 视频代码: UiObject word = new UiObject(new UiSelector().text("沪江开心词场")); word.clickAndWaitForNewWindow(); UiObject username = new UiObject(new UiSelector().text("沪江用户名/邮箱/手机")); username.setText("xuyisheng"); sleep(1000); UiObject pwd = new UiObject( new UiSelector().resourceId("com.hjwordgames:id/edit_password")); pwd.setText("123465"); sleep(2000); pwd.clearTextField(); sleep(1000); pwd.setText("123465"); UiDevice.getInstance().pressBack(); sleep(1000); UiObject login = new UiObject(new UiSelector().text("登 录")); login.clickAndWaitForNewWindow(); UiDevice.getInstance().pressBack(); UiDevice.getInstance().pressBack(); word.dragTo(300, 300, 50); sleep(1000); word.swipeDown(50); UiAutomator基本对象之UIScrollable 专业处理滚动一百年。 滚动 返回值 方法 解释 boolean flingBackward() 步长为5快速向后滑动 boolean flingForward() 步长为5快速向前滑动 boolean flingToBeginning(maxSwipes) 不超过maxSwipes滑动到最前,步长为5 boolean flingToEnd(maxSwipes) 不超过maxSwipes滑动到最后,步长为5 boolean flingToEnd(maxSwipes) 不超过maxSwipes滑动到最后,步长为5 …… …… 同样还可以使用Scroll,不一一列举 获取列表子元素 返回值 方法 解释 boolean getChildByDescription(childPattern, text) 默认滚动,查找childPattern UiSelector所对应的text子元素 boolean getChildByDescription(childPattern, text, allowScrollSearch) 是否允许滚动,查找childPattern UiSelector所对应的text子元素 …… …… 还有text、instance同样可以使用,不一一列举。 boolean scrollIntoView(obj) 滚动到obj所处的位置 boolean scrollIntoView(selector) 滚动到条件元素所处的位置 boolean scrollTextIntoView(text) 滚动到文本对象所处的位置 boolean scrollToBeginning(maxSwipes) 滚动到开始位置 boolean scrollToBeginning(maxSwipes, steps) 指定步长,滚动到开始位置 boolean scrollToEnd(maxSwipes) 滚动到最后位置 boolean scrollToEnd(maxSwipes, steps) 指定步长,滚动到最后位置 boolean setMaxSearchSwipes(swipes) 设置最大可扫动次数 boolean getMaxSearchSwipes() 获取最大可扫动次数、默认30 UiScrollable setSwipeDeadZonePercentage(swipeDeadZonePercentage) 设置滑动无效区域(到顶部的百分比) double getSwipeDeadZonePercentage() 获取滑动无效区域(到顶部的百分比) 滚动方向 返回值 方法 解释 boolean setAsHorizontalList() 设置水平滚动 boolean setAsVerticalList() 设置垂直滚动 示例代码 // 滑动 UiScrollable scroll=new UiScrollable(new UiSelector().className("android.widget.ListView")); scroll.flingBackward(); scroll.flingForward(); scroll.flingToBeginning(20); scroll.flingToEnd(30); // 滑动到某元素 UiScrollable scroll=new UiScrollable(new UiSelector().className("android.widget.ListView")); UiObject baiQiang=scroll.getChildByText(new UiSelector().className("android.widget.TextView"), "zhujia"); baiQiang.click(); scroll.getChildByInstance(new UiSelector().className("android.widget.TextView"), 25).click(); // 滑动到某元素 UiScrollable scroll=new UiScrollable(new UiSelector().className("android.widget.ListView")); UiSelector selector=new UiSelector().text("zhujia"); UiObject object=new UiObject(selector); scroll.scrollIntoView(selector); scroll.scrollIntoView(object); scroll.scrollTextIntoView("zhujia"); scroll.scrollDescriptionIntoView("zhujia"); scroll.scrollToBeginning(50,5); scroll.scrollToEnd(50,5); // 滑动方向 UiScrollable scroll=new UiScrollable(new UiSelector().className("android.support.v4.view.ViewPager")); scroll.setAsHorizontalList(); scroll.scrollBackward(); sleep(2000); scroll.scrollForward(); sleep(2000); scroll.setAsVerticalList(); scroll.scrollForward(); 视频大放送: 视频代码: UiScrollable scrollable = new UiScrollable( new UiSelector().className("android.widget.ListView")); scrollable.flingForward(); sleep(500); scrollable.flingBackward(); sleep(500); scrollable.flingForward(); UiObject target = new UiObject(new UiSelector().text("德国工业就是这么强大!不得不服")); scrollable.scrollIntoView(target); target.click(); UiAutomator基本对象之UICollection 通常用于获取满足某种搜索条件的组件集合,通过链式搜索确定最终需要的组件。 先按照一定的条件枚举容器内的子元素,再从符合条件的子元素中进一步定位。 一般使用容器类组件作为父类,用于寻找不好定位的子元素。 示例代码 UiCollection collection=new UiCollection(new UiSelector().className("android.widget.ListView")); UiSelector childPattern=new UiSelector().className("android.widget.TextView"); String text="Music"; UiObject music=collection.getChildByText(childPattern, text); music.click(); UiAutomator基本对象之UiWatcher 通常我们会让脚本来按照我们所需要的顺序来执行,但有时候,总有一些天灾人祸,比如10086发短信来了。 所以,我们的脚本必须要有一定的容错性。 UiWatcher正是这样一个容错的对象,当我们在顺序执行脚本时,如果中间突然插入了一些不明事件,我们可以使用UiWatcher来拦截异常,处理完异常后,再返回原来的脚本执行顺序。 UiAutomator基本对象之Configuration Configuration,自然是对默认操作的配置,通常情况下,我们使用默认的Configuration就足够了,当然,如果你有一些特殊需求,就可以通过Configuration类来设置。它能更改我们前面提到的所有默认属性的设置。包括默认延迟、输入延迟、等待超时等等。 UiAutomator基本对象之查看报告 下面是一个典型的UiAutomator测试报告: INSTRUMENTATION_STATUS: numtests=1 INSTRUMENTATION_STATUS: stream= com.hj.autotest.AutoTest: INSTRUMENTATION_STATUS: id=UiAutomatorTestRunner INSTRUMENTATION_STATUS: test=testDevice INSTRUMENTATION_STATUS: class=com.hj.autotest.AutoTest INSTRUMENTATION_STATUS: current=1 INSTRUMENTATION_STATUS_CODE: 1 INSTRUMENTATION_STATUS: numtests=1 INSTRUMENTATION_STATUS: stream=. INSTRUMENTATION_STATUS: id=UiAutomatorTestRunner INSTRUMENTATION_STATUS: test=testDevice INSTRUMENTATION_STATUS: class=com.hj.autotest.AutoTest INSTRUMENTATION_STATUS: current=1 INSTRUMENTATION_STATUS_CODE: 0 INSTRUMENTATION_STATUS: stream= Test results for WatcherResultPrinter=. Time: 31.489 OK (1 test) INSTRUMENTATION_STATUS_CODE: -1 这些报告被INSTRUMENTATION_STATUS_CODE分为了三个部分,1表示运行前,-1表示运行完成。 如果出错了,你可以在报告中找到相应的错误信息。 你同样需要知道的是,UiAutomator也是JUnit工程,你同样可以在里面使用断言来进行某些变量、结果值的测试,这些同样会在报告中体现出来。 最后,UiAutomator大部分内容都讲完了,最后一个视频: 视频代码: UiDevice.getInstance().pressHome(); new UiObject(new UiSelector().description("Apps")) .clickAndWaitForNewWindow(); UiScrollable scrollable = new UiScrollable( new UiSelector() .resourceId( "com.google.android.googlequicksearchbox:id/apps_customize_pane_content")); scrollable.setAsHorizontalList(); UiObject word = new UiObject(new UiSelector().text("沪江开心词场")); while (!word.exists()) { scrollable.scrollForward(); } word.clickAndWaitForNewWindow(); UiObject username = new UiObject(new UiSelector().text("沪江用户名/邮箱/手机")); username.setText("xys10086"); sleep(1000); UiObject pwd = new UiObject( new UiSelector().resourceId("com.hjwordgames:id/edit_password")); pwd.setText("Aa123465"); sleep(1000); UiObject login = new UiObject(new UiSelector().text("登 录")); login.clickAndWaitForNewWindow(); if (new UiObject(new UiSelector().className( "android.widget.FrameLayout").index(1)).exists()) { new UiObject(new UiSelector().text("注册")) .clickAndWaitForNewWindow(); new UiObject( new UiSelector() .resourceId("com.hjwordgames:id/registerEditUsername")) .setText("xys100861"); new UiObject( new UiSelector() .resourceId("com.hjwordgames:id/registerEditPassword")) .setText("Aa123456"); new UiObject( new UiSelector() .resourceId("com.hjwordgames:id/regiserEditEmail")) .setText("35998151@qq.com"); new UiObject(new UiSelector().text("确认注册")) .clickAndWaitForNewWindow(); UiObject ok = new UiObject( new UiSelector().resourceId("com.hjwordgames:id/btnOK")); if (ok.waitForExists(500)) { ok.clickAndWaitForNewWindow(); } } 如何使用UiAutomator 配置工程环境 在Eclipse中创建一个java工程,并添加platforms文件夹下面的android.jar和uiautomator.jar 两个引用。如下图: 创建测试用例 UiAutomator中的测试类都要继承UiAutomatorTestCase,每个测试用例的方法的方法名都要以test开头。如下图: 在测试用例的方法中,我们就可以编写测试脚本代码。 生成build.xml文件 在终端中,输入: android create uitest-project -n <name> -t <android-sdk-ID> -p <path> 这里的android sdk id指的是在终端中,输入android list返回的你使用的sdk的id。 这里还要PS下,一定要配置好环境变量,这是我们后面一键自动化的基础。 例如: android create uitest-project -n Demo -t 30 -p "F:\EclipseWorkSpace\AutoTest" 如下图: 修改build.xml文件 生成的build.xml文件我们还无法直接使用,我们需要修改它的一个属性,打开build.xml文件,将help改为build,如下图: 打包Jar 使用Ant,我们利用build.xml打包生成jar,命令如下: ant -buildfile "F:\EclipseWorkSpace\AutoTest" 编译过程如下图: Push Jar包到手机 我们需要将jar包push到手机中的/data/local/tmp/目录才能启动测试。如下图: adb push "F:\EclipseWorkSpace\AutoTest\bin\Demo.jar" /data/local/tmp/ 执行测试用例 在终端中输入启动测试命令(#后如果不指定具体的用例名,则测试所有的方法),如下: adb shell uiautomator runtest Demo.jar --nohup -c com.hj.autotest.AutoTest#testBrowser 到此为止,整个测试用例的测试就全部结束了。 让自动化测试自动起来 看完前面的步骤,相信很多人已经不想再看下去了,好吧,那你们损失大了,所谓自动化测试,就是为了减少人工的操作,像这样反复的编译、修改、push、运行,这跟手动去测试又有什么区别呢? OK,让自动化再上升一个境界。 我们可以发现,其实这些操作,与我们进行测试一样,也是一些机械动作,ok,那么我们完全可以使用同样的思路——使用脚本来解决这些问题。 我们创建一个脚本工具——UiAutomatorTool,来封装这些机械的步骤。代码非常简单,无非是使用Java调用终端命令,来执行前面的各种操作。 代码如下: package com.hj.autotest; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class UiAutomatorTool { // 工作空间目录 private static String WORKSPACE_PATH; /** * 指定自动测试的参数 * * @param jarName * 生成jar的名字 * @param testPackageclass * 测试包名+类名 * @param testFunction * 测试方法名,空字符串代表测试所有方法 * @param androidId * SDK id */ public UiAutomatorTool(String jarName, String testPackageclass, String testFunction, String androidId) { System.out.println("*******************"); System.out.println(" --AutoTest Start--"); System.out.println("*******************"); // 获取工作空间目录路径 WORKSPACE_PATH = getWorkSpase(); System.out.println("自动测试项目工作空间:\t\n" + getWorkSpase()); // ***********启动测试*********** // // 创建Build.xml文件 creatBuildXml(jarName, androidId); // 修改Build.xml文件中的Build类型 modfileBuild(); // 使用Ant编译jar包 antBuild(); // push jar到手机 pushJarToAndroid(WORKSPACE_PATH + "\\bin\\" + jarName + ".jar"); // 测试方法,为空则测试全部方法 if (androidId.equals("")) { runTest(jarName, testPackageclass); } else { runTest(jarName, testPackageclass + "#" + testFunction); } // ***********启动测试*********** // System.out.println("*******************"); System.out.println("---AutoTest End----"); System.out.println("*******************"); } /** * 创建build.xml文件 */ public void creatBuildXml(String jarName, String androidID) { System.out.println("--------创建build.xml 开始---------"); execCmd("cmd /c android create uitest-project -n " + jarName + " -t " + androidID + " -p " + "\"" + WORKSPACE_PATH + "\""); System.out.println("--------创建build.xml 完成---------"); } /** * 修改build.xml文件位build type */ public void modfileBuild() { System.out.println("--------修改build.xml 开始---------"); StringBuffer stringBuffer = new StringBuffer(); try { File file = new File("build.xml"); if (file.isFile() && file.exists()) { InputStreamReader read = new InputStreamReader( new FileInputStream(file)); BufferedReader bufferedReader = new BufferedReader(read); String lineTxt; while ((lineTxt = bufferedReader.readLine()) != null) { if (lineTxt.matches(".*help.*")) { lineTxt = lineTxt.replaceAll("help", "build"); } stringBuffer = stringBuffer.append(lineTxt).append("\t\n"); } read.close(); } else { System.out.println("找不到build.xml文件"); } } catch (Exception e) { System.out.println("读取build.xml内容出错"); e.printStackTrace(); } // 重新写回build.xml rewriteBuildxml("build.xml", new String(stringBuffer)); System.out.println("--------修改build.xml 完成---------"); } /** * 使用Ant编译jar包 */ public void antBuild() { System.out.println("--------编译build.xml 开始---------"); execCmd("cmd /c ant -buildfile " + "\"" + WORKSPACE_PATH + "\""); System.out.println("--------编译build.xml 完成---------"); } /** * adb push jar包到Android手机 * * @param localPath * localPath */ public void pushJarToAndroid(String localPath) { System.out.println("--------push jar 开始---------"); localPath = "\"" + localPath + "\""; System.out.println("jar包路径:" + localPath); String pushCmd = "adb push " + localPath + " /data/local/tmp/"; execCmd(pushCmd); System.out.println("--------push jar 完成---------"); } /** * 测试方法 * * @param jarName * jar包名 * @param testName * testName */ public void runTest(String jarName, String testName) { System.out.println("--------测试方法 开始---------"); String runCmd = "adb shell uiautomator runtest "; String testCmd = jarName + ".jar " + "--nohup -c " + testName; execCmd(runCmd + testCmd); System.out.println("--------测试方法 完成---------"); } /** * 获取WorkSpace目录 * * @return WorkSpace目录 */ public String getWorkSpase() { File directory = new File(""); return directory.getAbsolutePath(); } /** * Shell命令封装类 * * @param cmd * Shell命令 */ public void execCmd(String cmd) { System.out.println("ExecCmd:" + cmd); try { Process p = Runtime.getRuntime().exec(cmd); // 执行成功返回流 InputStream input = p.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader( input, "GBK")); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } // 执行失败返回流 InputStream errorInput = p.getErrorStream(); BufferedReader errorReader = new BufferedReader( new InputStreamReader(errorInput, "GBK")); String eline; while ((eline = errorReader.readLine()) != null) { System.out.println(eline); } } catch (IOException e) { e.printStackTrace(); } } /** * 重新写回Build.xml * * @param path * path * @param content * content */ public void rewriteBuildxml(String path, String content) { File dirFile = new File(path); if (!dirFile.exists()) { dirFile.mkdir(); } try { BufferedWriter bw1 = new BufferedWriter(new FileWriter(path)); bw1.write(content); bw1.flush(); bw1.close(); } catch (IOException e) { e.printStackTrace(); } } } 那我们怎么使用呢?拿一个测试类来说: public class AutoTest extends UiAutomatorTestCase { public static void main(String[] args) { new UiAutomatorTool("Demo", "com.hj.autotest.AutoTest", "testUiSelector", "30"); } …… } 我们只需要在测试类中new一个UiAutomatorTool,并指定jar包名、包名、用例名、Android id即可。 接下来,只需要运行这样Java程序,就完成了整个过程的自动化,一键编译、一键运行。 好吧,再来一个视频: 让偷懒更进一步 前面我们已经让编译、push、运行自动化了,但是说到底,就连编写脚本也是一件非常繁琐的事情啊。OK,我们同样可以创建一个H5的页面,通过编写图形化的页面,来替代我们每个动作脚本的编写,毕竟这些脚本也是死的啊。 让偷懒发扬光大 这些脚本可不仅仅能做测试。 经过前面一系列的代码、演示,我们已经可以通过脚本来进行测试用例的自动化测试,但是,自动化不仅仅可以用来测试,当我们在调试程序的时候,经常需要登陆App以后才能进行测试,我们同样可以把这些操作放到脚本中,启动调试后,直接运行脚本,完成这样繁琐的输入、登陆步骤。
转自 http://blog.csdn.net/d_clock/article/details/42968039 前段时间,在公司做项目的时候发现原有项目中的代码在Service中使用handler不断发送Message到Looper处理MessageQueue中来维持IM功能的“心跳”,心里瞬间觉得这个地方的代码很不靠谱,主要原因分为两个: 1.handler的生命周期和Service不一致,如果Service某个时刻被系统回收内存杀死了,逻辑上handler应该就会停止心跳包的发送,但是此时实际代码运用中handler依旧可以源源不断的发送消息,而且由于handler持有了外部销毁的Service的引用,造成Service即使被杀但是内存不被回收的内存泄漏问题也是比较严重的; 2.Android官方的API文档建议我们,如果要执行定时任务的话,可以使用AlarmManager来定期执行任务,减少唤醒系统时钟的次数,从而减少电量的消耗; 针对以上问题,做了一下小小了解,因为项目开发中我对于AlarmManager的使用已经相当熟悉,但是对系统唤醒锁的概念还是不是很理解,之前甚至天真的认为,只要锁了屏,系统锁就不会被唤醒,亮屏的时候,系统唤醒锁才重新唤醒。可是想想又不太对,系统在锁屏的时候,如果我们设置了闹钟提醒的功能,时间到了之后,闹钟就会响起来,这恰恰说明了锁屏的时候系统锁仍旧被唤醒工作,估计系统提供AlarmManager给开发者使用,只是为了让系统唤醒锁的次数交给统一的管理者管理,这样可以降低锁频繁被唤醒的几率,从而达到节能的目的,对此我也对AlarmManager和系统时钟的概念做了以下一些小小的总结: Android手机有两个处理器,一个叫ApplicationProcessor(AP),一个叫BasebandProcessor(BP)。AP是ARM架构的处理器,用于运行Linux+Android系统;BP用于运行实时操作系统(RTOS),通讯协议栈运行于BP的RTOS之上。非通话时间,BP的能耗基本上在5mA左右,而AP只要处于非休眠状态,能耗至少在50mA以上,执行图形运算时会更高。另外LCD工作时功耗在100mA左右,WIFI也在100mA左右。一般手机待机时,AP、LCD、WIFI均进入休眠状态,这时Android中应用程序的代码也会停止执行。Android为了确保应用程序中关键代码的正确执行,提供了WakeLock的API,使得应用程序有权限通过代码阻止AP进入休眠状态。但如果不领会Android设计者的意图而滥用Wake Lock API,为了自身程序在后台的正常工作而长时间阻止AP进入休眠状态,就会成为待机电池杀手。 首先,完全没必要担心AP休眠会导致收不到消息推送。通讯协议栈运行于BP,一旦收到数据包,BP会将AP唤醒,唤醒的时间足够AP执行代码完成对收到的数据包的处理过程。其它的如Connectivity事件触发时AP同样会被唤醒。那么唯一的问题就是程序如何执行向服务器发送心跳包的逻辑。你显然不能靠AP来做心跳计时。Android提供的AlarmManager就是来解决这个问题的。Alarm应该是BP计时(或其它某个带石英钟的芯片,不太确定,但绝对不是AP),触发时唤醒AP执行程序代码。那么WakeLock API有啥用呢?比如心跳包从请求到应答,比如断线重连重新登陆这些关键逻辑的执行过程,就需要WakeLock来保护。而一旦一个关键逻辑执行成功,就应该立即释放掉Wake Lock了。两次心跳请求间隔5到10分钟,基本不会怎么耗电。除非网络不稳定,频繁断线重连,那种情况办法不多。
没事整理了下书签,发现了好多好多好多好多好多平时收藏了还没看的东西,先贴下来,慢慢看。 EOE http://www.eoeandroid.com/ AndBase http://www.amsoft.cn/ 酷壳 http://coolshell.cn/ 前端导航站 http://123.jser.us/ 最受欢迎的开源项目 http://www.csdn.net/tag/%E6%9C%80%E5%8F%97%E6%AC%A2%E8%BF%8E%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/news ITEye http://www.iteye.com/ Jar Search http://www.findjar.com/index.x ImportNew http://www.importnew.com/ http://www.importnew.com/3988.html Linux公社 http://linux.linuxidc.com/ WAP地带 http://www.iwapzone.com/ 伯乐在线 http://blog.jobbole.com/ Apk Bus http://www.apkbus.com/ 多贝公开课 http://www.duobei.com/explore/tag/Android 千锋3G http://www.mobiletrain.org/lecture/doc/android/ 酷勤网 http://www.kuqin.com/ Trinea http://www.trinea.cn/ 爱开网 http://neast.cn/forum.php 安卓航班 http://www.apkway.com/forum.php?mod=viewhttp://tieba.baidu.com/p/2662508388?see_lz=1&pn=7 花瓣UI http://huaban.com/cc/web_app_icon/ GrepCode http://www.grepcode.com/ 程序员 http://www.pudn.com/ 一起晃素材 http://app.17huang.com/home.php Android Design中文 http://www.apkbus.com/design/index.html 码农场 http://www.hankcs.com/category/program/mobiledev/ 云在千锋 http://yunfeng.sinaapp.com/ OverAPI http://overapi.com/ Android中文API http://www.android-doc.com/ Android API镜像 http://androiddoc.qiniudn.com/reference/packages.html DevStore http://www.devstore.cn/ UI中国 http://www.ui.cn/ 极客范 http://www.geekfan.net/category/android-2/ BAAS http://cn.kii.com/ http://www.uncode.cn/index.html MD中文版 http://design.1sters.com/ 代码家 http://blog.daimajia.com/android-library-collection/ CodeKK http://www.codekk.com/open-source-project-analysis 开源实验室 http://www.kymjs.com/page/framework.html Android中文社区 http://www.androidcn.org/ Idea Color & Theme http://www.ideacolorthemes.org/home/ 最代码 http://www.zuidaima.com/ Stormzhang http://stormzhang.com/ EOE Android Wiki http://wiki.eoeandroid.com/%E9%A6%96%E9%A1%B5 阿里巴巴Icon http://www.iconfont.cn/ easy icon http://www.easyicon.net/ TinyPNG https://tinypng.com/ 自学宝典 http://www.csdn.net/article/2015-02-23/2824006 MD开源项目 http://www.csdn.net/article/2014-11-21/2822753-material-design-libs 程序师 http://www.techug.com/ MarkDown入门 http://www.360doc.com/content/13/1119/13/3300331_330476656.shtml 在线JSON http://www.bejson.com/go.php?u=http://www.bejson.com/webInterface.php ShadowSocks https://shadowsocks.com/ fangjie http://fangjie.info/ CocoVPN http://ttt.yyyx.info/ Sketch2photo http://cg.cs.tsinghua.edu.cn/montage/main.htm Android源码服务专家 http://www.javaapk.com/ Kyle blog http://www.kyleduo.com/ 郝锡强 http://www.haoxiqiang.info/ 高效App http://blog.jobbole.com/64279/ 基于Android4.0.3的各种工具信息整理(共130个) http://m.blog.csdn.net/blog/backgarden_straw/8154660 lucasr http://lucasr.org/2014/05/12/custom-layouts-on-android/ Instagram with Material Design http://frogermcs.github.io/ Kale http://www.cnblogs.com/tianzhijiexian/ Trick Android http://trickyandroid.com/ coderrobin http://coderrobin.com/categories/Android/ flavienlaurent http://flavienlaurent.com/ Romain Guy http://www.curious-creature.com/category/android/ API最佳实践 http://mobile.51cto.com/aprogram-435994.htm cyrilmottier http://cyrilmottier.com/archives/ 让动画更加自然 http://djt.qq.com/article/view/1249?bsh_bid=481632500 传课网 http://www.chuanke.com/course/_android____.html Android学习索引 http://www.cnblogs.com/qianxudetianxia/archive/2011/05/02/2034303.html
转自 http://www.cnblogs.com/panpei/archive/2013/02/13/2910680.html 前些日子,有点无聊,就在网上逛逛技术大牛的blogs,发现很多大牛都喜欢用pdf版式的简历,发现这种版式的简历排版非常漂亮简洁。深究了一下,发现其实是利用LaTeX生成的(多说一句,不得不佩服DonaldE.Knuth大师发明的TeX排版的确是美观)。 LaTeX或许不是很多人知道,但是那些忙着发papers的Master Candidate、Ph.D Candidate应该是非常了解的。当然,那些苦逼的数学系的孩子们应该也是知道,因为word对于数学公式的排版效果相比于LaTeX,还是差的远了。至于LaTeX的具体信息,这里就不废话,有兴趣的同学可以到Google上百度一下的。 言归正传,LaTeX写简历谈何容易,尤其是从头写起,还尤其对于我这样的LaTeX菜鸟而言,更是难如登天。于是Google了一把,找到一个叫moderncv的共享简历模板。有了模板,那么就简单多了(插嘴一句,其实发现有些大牛用的也是这个模板改写的)。 modercv下载地址:http://www.ctan.org/tex-archive/macros/latex/contrib/moderncv 下载下来后应该是一个zip包,解压后目录如下: 其中.sty文件都是定义简历风格的文件,还有那个moderncv.cls文件。其实这些都是编写LaTeX风格源码后生成的,有兴趣的同学可以继续深究一下LaTeX的其他知识,应该会有所收获的。这些文件在后我们编译自己的简历时会用到。 接下来看看examples文件夹: 好了,这里面东西也很多,稍微懂点LaTeX的同学就会知道,其实,只有那几个.tex文件使我们想要的,而且也是非常重要的。为什么?因为那些个文件就是模板啊。我们的简历的生成就靠它们了。 .tex文件有三个:template.tex,template-es.tex,template-zh.tex,顾名思义,这个三个模板表示中英文简历的模板,其实template.tex就是英文模板,而template-es.tex是什么语种的模板,我也不知道,反正不是英文模板。 好了,剩下就是开始写我们的简历了,我们先建立一个文件夹,如MyCV之类的啦,然后把前面提到的.sty文件、.cls文件还有.tex的模板文件放进去。就像下面: 把模板文件的名字改成个人喜好的都可以的,如我就改成了my_cv_en.tex和my_cv_en.tex。剩下来我们就开始对我们的简历模板开始编辑了,我是用的NotePad++,当然大家可以用其他的编辑器,如WinEdt、Texmaker,甚至你可以用word,txt等等,当然我是既不赞成后两者的,尤其txt,当你使用后,就会对那一对没有高亮显示,没有缩进的代码抓狂的。 这个是我用Notepad++打开的template.tex的文档,效果还是可以的。 好了,如何改写这堆代码,其实挺容易的,模板中有着那么多的注释,很好的改的(好吧,我有点偷懒,这个以后再介绍)。 等我们改好个人信息后,剩下来就是编译了。 我用的是CTex的套装,然后利用WinEdt来编译,这个的确是有点偷懒了,不过可视化的界面的确是方便啊。用WinEdt打开我们编辑的.tex文件就可以了,当然,也可以用这个编辑器去编辑.tex文件。 CTex下载地址:http://www.ctex.org/HomePage CTex中其实就包含有WinEdt。 英文模板直接用那个LaTeX按钮编译,中文模板涉及到编码问题,用那个XeLaTeX按钮。只要中间我们没有写错什么语句之类的,接下来我们就可以在文件夹中,如MyCVS,看到生成的pdf文件。排版效果相当的不错滴。 模板风格有好几个,如casual(default), classic, oldstyle 以及banking,还有颜色也有几种,blue(default), orange, green, red, purple, grey 和black。具体的要求可以根据注释自行搭配。 blue-casual 其他几种风格: 总结: 1.对于LaTeX应该有所了解,明白LaTeX各个命令的含义。 2.理解模板中的各项命令的含义,模板注释中有解释,可以自己尝试改动一下。 3.电脑上装有LaTeX编译器,例如我就装有CTex套装。 4.好奇心和耐心。 有了以上的条件,你就应该可以做出一个漂亮的用LaTeX写的简历了。
转自 http://blog.jobbole.com/39309/ 你是否认为“ASCII码 = 一个字符就是8比特”?你是否认为一个字节就是一个字符,一个字符就是8比特?你是否还认为你是否还认为UTF-8就是用8比特表示一个字符?如果真的是这样认为认真读完这篇文章吧! 为什么要有编码? 首先大家需要明确的是在计算机里所有的数据都是字节的形式存储,处理的。我们需要这些字节来表示计算机里的信息。但是这些字节本身又是没有任何意义的,所以我们需要对这些字节赋予实际的意义。所以才会制定各种编码标准。 编码模型 首先需要明确的是存在两种编码模型 简单字符集 在这种编码模型里,一个字符集定义了这个字符集里包含什么字符,同时把每个字符如何对应成计算机里的比特也进行了定义。例如ASCII,在ASCII里直接定义了A -> 0100 0001。 现代编码模型 在现代编码模型里要知道一个字符如何映射成计算机里比特,需要经过如下几个步骤。 知道一个系统需要支持哪些字符,这些字符的集合被称为字符表(Character repertoire) 给字符表里的抽象字符编上一个数字,也就是字符集合到一个整数集合的映射。这种映射称为编码字符集(CCS:Coded Character Set),unicode是属于这一层的概念,跟计算机里的什么进制啊没有任何关系,它是完全数学的抽象的。 将CCS里字符对应的整数转换成有限长度的比特值,便于以后计算机使用一定长度的二进制形式表示该整数。这个对应关系被称为字符编码表(CEF:Character Encoding Form)UTF-8, UTF-16都属于这层。 对于CEF得到的比特值具体如何在计算机中进行存储,传输。因为存在大端小端的问题,这就会跟具体的操作系统相关了。这种解决方案称为字符编码方案(CES:Character Encoding Scheme)。 平常我们所说的编码都在第三步的时候完成了,都没有涉及到CES。所以CES并不在本文的讨论范围之内。现在也许有人会想为什么要有现代的编码模型?为什么在现在的编码模型要拆分出这么多概念?直接像原始的编码模型直接都规定好所有的信息不行吗?这些问题在下文的编码发展史中都会有所阐述。 编码的发展史 ASCII ASCII出现在上个世纪60年代的美国,ASCII一共定义了128个字符,使用了一个字节的7位。定义的这些字符包括英文字母A-Z,a-z,数字0-9,一些标点符号和控制符号。在Shell里输入man ASCII,可以看到完整的ASCII字符集。ASCII采用的编码模型是简单字符集,它直接定义了一个字符的比特值表示。里例如上文提到的A -> 0100 0001。也就是ASCII直接完成了现代编码模型的前三步工作。在英语系国家里ASCII标准很完美。但是不要忘了世界上可有好几千种语言,这些语言里不仅只有这些符号啊。如果使用这些语言的人也想使用计算机,ASCII就远远不够了。到这里编码进入了混乱的时代。 混乱时代 人们知道计算机的一个字节是8位,可以表示256个字符。ASCII却只使用了7位,所以人们决定把剩余的一位也利用起来。这时问题出现了,人们对 于已经规定好的128个字符是没有异议的,但是不同语系的人对于其他字符的需求是不一样的,所以对于剩下的128个字符的扩展会千奇百怪。而且更加混乱的 是,在亚洲的语言系统中有更多的字符,一个字节无论如何也满足不了需求了。例如仅汉字就有10万多个,一个字节的256表示方式怎么能够满足呢。于是就又 产生了各种多字节的表示一个字符方法(gbk就是其中一种),这就使整个局面更加的混乱不堪。(希望看到这里的你不再认为一个字节就是一个字符,一个字符 就是8比特)。每个语系都有自己特定的编码页(code pages)的状况,使得不同的语言出现在同一台计算机上,不同语系的人在网络上进行交流都成了痴人说梦。这时Unicode出现了。 Unicode Unicode就是给计算机中所有的字符各自分配一个代号。Unicode通俗来说是什么呢?就是现在实现共产主义了,各国人民不在需要自己特定的 国家身份证,而是给每人一张全世界通用的身份证。Unicode是属于编码字符集(CCS)的范围。Unicode所做的事情就是将我们需要表示的字符表 中的每个字符映射成一个数字,这个数字被称为相应字符的码点(code point)。例如“严”字在Unicode中对应的码点是U+0x4E25。 到目前为止,我们只是找到了一堆字符和数字之间的映射关系而已,只到了CCS的层次。这些数字如何在计算机和网络中存储和展示还没有提到。 字符编码 前面还都属于字符集的概念,现在终于到CEF的层次了。为了便于计算的存储和处理,现在我们要把哪些纯数学数字对应成有限长度的比特值了。最直观的 设计当然是一个字符的码点是什么数字,我们就把这个数字转换成相应的二进制表示,例如“严”在Unicode中对应的数字是0x4E25,他的二进制是100 1110 0010 0101, 也就是严这个字需要两个字节进行存储。按照这种方法大部分汉字都可以用两个字节来表示了。但是还有其他语系的存在,没准儿他们所使用的字符用这种方法转换 就需要4个字节。这样问题又来了到底该使用几个字节表示一个字符呢?如果规定两个字节,有的字符会表示不出来,如果规定较多的字节表示一个字符,很多人又 不答应,因为本来有些语言的字符两个字节处理就可以了,凭什么用更多的字节表示,多么浪费。 这时就会想可不可以用变长的字节来存储一个字符呢?如果使用了变长的字节表示一个字符,那就必须要知道是几个字节表示了一个字符,要不然计算机可没 那么聪明。下面介绍一下最常用的UTF-8(UTF是Unicode Transformation Format的缩写)的设计。请看下图(来自阮一峰的博客) x表示可用的位 通过UTF-8的对应关系可以把每个字符在Unicode中对应的码点,转换成相应的计算机的二进制表示。可以发现按照UTF-8进行转换是完全兼 容原先的ASCII的;而且在多字节表示一个字符时,开头有几个1就表示这个字符按照UTF-8转换后由几个字节表示。下面一个实例子来自阮一峰的博客 已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是 “11100100 10111000 10100101”,转换成十六进制就是0xE4B8A5。 除了UTF-8这种转换方法,还存在UTF-16,UTF-32等等转换方法。这里就不再多做介绍。(注意UTF后边的数字代表的是码元的大小。码 元(Code Unit)是指一个已编码的文本中具有最短的比特组合的单元。对于UTF-8来说,码元是8比特长;对于UTF-16来说,码元是16比特长。换一种说法 就是UTF-8的是以一个字节为最小单位的,UTF-16是以两个字节为最小单位的。) 结束语 花了两天时间终于写完了,相信看到这里大家对于字符编码有了较为清楚的认识,当然文章中肯定存在不准确之处,希望大家批评指正。
转自 http://yelinsen.iteye.com/blog/1012740 1 Android 安全机制概述 Android 是一个权限分离的系统 。 这是利用 Linux 已有的权限管理机制,通过为每一个 Application 分配不同的 uid 和 gid , 从而使得不同的 Application 之间的私有数据和访问( native 以及 java 层通过这种 sandbox 机制,都可以)达到隔离的目的 。 与此 同时, Android 还 在此基础上进行扩展,提供了 permission 机制,它主要是用来对 Application 可以执行的某些具体操作进行权限细分和访问控制,同时提供了 per-URI permission 机制,用来提供对某些特定的数据块进行 ad-hoc 方式的访问。 1.1 uid 、 gid 、 gids Android 的权限分离的基础是建立在 Linux 已有的 uid 、 gid 、 gids 基础上的 。 UID 。 Android 在 安装一个应用程序,就会为 它 分配一个 uid (参考 PackageManagerService 中的 newUserLP 实现)。其中普通 A ndroid 应用程序的 uid 是从 10000 开始分配 (参见 Process.FIRST_APPLICATION_UID ), 10000 以下是系统进程的 uid 。 GID 。对 于普通应用程序来说, gid 等于 uid 。由于每个应用程序的 uid 和 gid 都不相同, 因此不管是 native 层还是 java 层都能够达到保护私有数据的作用 。 GIDS 。 gids 是由框架在 Application 安装过程中生成,与 Application 申请的具体权限相关。 如果 Application 申请的相应的 permission 被 granted ,而且 中有对应的 gid s , 那么 这个 Application 的 gids 中将 包含这个 gid s 。 uid gid gids 的 详细 设置过程: 请参考 Act i vityManagerService 中的 startProcessLocked 。在通过 zygote 来启动一个 process 时,直接将 uid 传给 给了 gid 。再通过 zygote 来 fork 出新的进程( zygote.java 中的 forkAndSpecialize ),最终在 native 层( dalvik_system_zygote.c )中的 forkAndSpecializeCommon 中通过 linux 系统调用来进行 gid 和 uid 和 gids 的设置。 1.2 permission 一个权限主要包含三个方面的信息:权限的名称;属于的权限组;保护级别。一个权限组是指把权限按照功能分成的不同的集合。每一个权限组包含若干具体权限,例如在 COST_MONEY 组中包含 android.permission.SEND_SMS , android.permission.CALL_PHONE 等和费用相关的权限。 每个权限通过 protectionLevel 来标识保护级别: normal , dangerous , signature , signatureorsystem 。不同的保护级别代表了程序要使用此权限时的认证方式。 normal 的权限只要申请了就可以使用; dangerous 的权限在安装时需要用户确认才可以使用; signature 和 signatureorsystem 的权限需要使用者的 app 和系统使用同一个数字证书。 Package 的权限信息主要 通过在 AndroidManifest.xml 中通过一些标签来指定。如 <permission> 标签, <permission-group> 标签 <permission-tree> 等标签。如果 package 需要申请使用某个权限,那么需要使用 <use-permission> 标签来指定。 2 Android permission 管理机制 2.1 Framework permission 机制 2.1.1 安装入口 permission 的初始化,是指 permission 的向系统申请,系统进行检测并授权,并建立相应的数据结构。绝大多数的情况下 permission 都是从一个 package 中扫描所得,而这发生在 package 安装和升级的时候。一般有如下几种 安装入口: n packageInstaller , package 被下载安装时会触发使用。 packageInstaller 会通过 AppSecurityPermissions 来检查 dangerous 的权限,并对用户给出提示。 n pm 命令 。 n adb install 。最终还是 调用 pm install 来安装 apk 包。 n 拷贝即安装。 PackageManagerService 中使用 AppDirObserver 对 /data/app/ 进行监视 ,如果有拷贝即触发安装。 这些安装方式 最终都会通过调用 PackageManagerService 中的函数来完成程序的安装。 2.1.2 permission 创建 第一步,从 AndroidManifest.xml 中提取 permission 信息。主要提取如下信息: ² shared uid 指定与其它 package 共享同一个 uid 。 ² permission 提取 permissions 标签指定属性。它使用 permissionInfo 来描述一个权限的基本信息。需要指定 protectedLevel 信息,并指定所属 group 信息。它将被添加到这个 package 的 permissions 这个 list 结构中。 ² permission-tree 提取 permissions-tree 标签属性。 permissions-tree 也通过 permissionInfo 来描述,并被添加到 package 的 permissions 这个 list 结构中。 permission-tree 只是一个名字空间,用来向其中动态添加一些所谓 Dynamic 的 permission ,这些 permission 可以动态修改。这些 permission 名称要以 permission-tree 的名称开头。它本身不是一种权限,没有 protectedLevel 和所属 group 。只是保存了所属的 packge 和权限名(带有 package 前缀的)。 ² permission-group 定义 permission 组信息,用 PermissionGroup 表示。本身不代表一个权限,会添加进入 package 的 permissionGroups 这个 list 中。 ² uses-permission 定义了 package 需要申请的权限名。将权限名添加到 package 的 requestedPermissions 这个 list 中。 ² adopt-permissions 将该标签指定的 name 存入 package 的 mAdoptPermissions 这个 list 中。 Name 指定了这个 package 需要从 name 指定的 package 进行权限领养。在 system package 进行升级时使用。 第二步。获取 Package 中的证书,验证,并将签名信息保存在 Package 结构中。 1. 如果该 package 来自 system img (系统 app ),那么只需要从该 Package 的 AndroidManifest.xml 中获取签名信息,而无需验证其完整性。但是如果这个 package 与其它 package 共享一个 uid ,那么这个共享 uid 对应的 sharedUser 中保存的签名与之不一致,那么签名验证失败。 2. 如果是普通的 package ,那么需要提取证书和签名信息,并对文件的完成性进行验证。 第三步。如果是普通的 package ,那么清除 package 的 mAdoptPermissions 字段信息(系统 package 升级才使用)。 第四步。如果在 AndroidManifest.xml 中指定了 shared user ,那么先查看全局 list 中( mSharedUsers )是否该 uid 对应的 SharedUserSetting 数据结构,若没有则新分配一个 uid ,创建 SharedUserSetting 并保存到全局全局 list ( mSharedUsers )中。 mUserIds 保存了系统中已经分配的 uid 对应的 SharedUserSetting 结构。每次分配时总是从第一个开始轮询,找到第一个空闲的位置 i ,然后加上 FIRST_APPLICATION_UID 即可。 第五步。创建 PackageSettings 数据结构。并将 PackageSettings 与 SharedUserSetting 进行绑定。其中 PackageSettings 保存了 SharedUserSetting 结构;而 SharedUserSetting 中会使用 PackageSettings 中的签名信息填充自己内部的签名信息,并将 PackageSettings 添加到一个队列中,表示 PackageSettings 为其中的共享者之一。 在创建时,首先会以 packageName 去全局数据结构 mPackages 中查询是否已经有对应的 PackageSettings 数据结构存在。如果已经存在 PackageSettings 数据结构(比如这个 package 已经被 uninstall ,但是还没有删除数据,此时 package 结构已经被释放)。那么比较该 package 中的签名信息(从 AndroidManifest 中扫描得到)与 PackageSettings 中的签名信息是否匹配。如果不匹配但是为 system package ,那么信任此 package ,并将 package 中的签名信息更新到已有的 PackageSettings 中去,同时如果这个 package 与其它 package 共享了 uid ,而且 shared uid 中保存的签名信息与当前 package 不符,那么签名也验证失败。 第六步。如果 mAdoptPermissions 字段不为空,那么处理 permission 的领养(从指定的 package 对应的 PackageSettings 中,将权限的拥有者修改为当前 package ,一般在 system app 升级的时候才发生,在此之前需要验证当被领养的 package 已经被卸载,即检查 package 数据结构是否存在)。 第七步。添加自定义权限。将 package 中定义的 permissionGroup 添加到全局的列表 mPermissionGroups 中去;将 package 中定义的 permissions 添加到全局的列表中去(如果是 permission-tree 类型,那么添加到 mSettings.mPermissionTrees ,如果是一般的 permission 添加到 mSettings.mPermissions 中)。 第八步。清除不一致的 permission 信息。 1. 清除不一致的 permission-tree 信息。如果该 permission-tree 的 packageSettings 字段为空,说明还未对该 package 进行过解析(若代码执行到此处时 packageSettings 肯定已经被创建过),将其 remove 掉。如果 packageSettings 不为空,但是对应的 package 数据结构为空(说明该 package 已经被卸载,但数据还有保留),或者 package 数据结构中根本不含有这个 permission-tree ,那么将这个 permission-tree 清除。 2. 清除不一致的 permission 信息。如果 packageSettings 或者 package 结构为空(未解析该 package 或者被卸载,但数据有保留),或者 package 中根本没有定义该 permission ,那么将该 permission 清除。 第九步。对每一个 package 进行轮询,并进行 permission 授权。 1. 对申请的权限进行检查,并更新 grantedPermissions 列表 2. 如果其没有设置 shared user id ,那么将其 gids 初始化为 mGlobalGids ,它从 permission.xml 中读取。 3. 遍历所有申请的权限,进行如下检查 1 )如果是该权限是 normal 或者 dangerous 的。通过检查。 2 )如果权限需要签名验证。如果签名验证通过。还需要进行如下检查 * 如果程序升级,而且是 system package 。那么是否授予该权限要看原来的 package 是否被授予了该权限。如果被授予了,那么通过检查,否则不通过。 * 如果是新安装的。那么检查通过。 4. 如果 3 中检查通过,那么将这个 permission 添加到 package 的 grantedPermissions 列表中,表示这个 permission 申请成功( granted )。申请成功的同时会将这个申请到的 permission 的 gids 添加到这个 package 的 gids 中去。 5. 将 permissionsFixed 字段标准为 ture ,表示这个 packge 的 permission 进行过修正。后续将禁止对非 system 的 app 的权限进行再次修正。 2.1.3 Dynamic permission 的管理 PackageManagerService 提供了 addPermission/ removePermission 接口用来动态添加和删除一些权限。但是这些权限必须是所谓的动态权限( BasePermission.TYPE_DYNAMIC )。 一个 Package 如果要添加 Dynamic permissions ,首先必须要在 manifest 中申明 <permission-tree> 标签,它实际上是一个权限的名字空间(例如,“ com.foo.far ”这个权限就是 permission-tree “com.foo ”的成员),本身不是一个权限。一个 Package 只能为自己的 permission-tree 或者拥有相同的 uid 的 package 添加或者删除权限。 Package 不能够通过这种接口去修改在 manifest 中静态申请的权限,否则抛出异常。 首先查找这个 permission 在全局 permission 列表 mSettings.mPermissions 中是否存在。如果存在,而且类型为 BasePermission.TYPE_DYNAMIC 那么根据传入的权限信息修改全局表中的权限信息,并触发 permissions.xml 的持久化。 如果在全局的 permission 列表 mSettings.mPermissions 中没有找到,先找到这个 permission 所在 permissionTree ,然后添加到全局 permission 列表 mSettings.mPermissions 中去,并触发 permissions.xml 的持久化。 2.1.4 Uri permission 的管理 下面两个 接口 主要用于 Uri permission 的管理 (其实现在 ActivityManagerService 中)。 // 为指定的 uid 和 targetPkg 添加对某个 content Uri 的读或者写权限。 public void grantUriPermission(IApplicationThread caller, String targetPkg, Uri uri, int mode) throws RemoteException; // 清除所有通过 grantUriPermission 对某个 Uri 授予的权限。 public void revokeUriPermission(IApplicationThread caller, Uri uri, int mode) throws RemoteException; grantUriPermission 主要的实现过程分析。 grantUriPermission 分析: 1. 验证 caller 的 ProcessRecord 和 targetPkg 不为空。否则检测不通过。 2. 验证所请求的 mode 为 Intent.FLAG_GRANT_READ_URI_PERMISSION 或者为 Intent.FLAG_GRANT_WRITE_URI_PERMISSION ,否则不通过。 3. 确保参数 Uri 是一个 content Uri 。否则,则检测不通过。 4. 通过 Uri 得到目标 ContentProvider ,如果不存在,则检测不通过。 5. 从 PackageManagerService 中获得 targetPkg 对应的 uid 。 6. 检查 target uid 所对应的 package 是否真正需要这个权限? 先判断要申请的是读还是写权限,然后查看对应的 ContentProvider 中对应的 readPermission writePermission 字段是否保存了权限名称。 如果该字段不为空,则以 target uid 和该权限名去PackageManagerService 中去查找该 uid 是否被 granted 了该权限。如果已经获得了该权限,那么无需再去为这个 Activity 去申请这个 Uri 权限了,返回。否者继续执行如下操作。 7. 检查这个 ContentProvider 的 grantUriPermissions 开关变量,是否允许对其它 package 进行权限的 grant 操作。如果禁止,那么抛出异常。 8. 检查这个 ContentProvider 是否设置了 Uri 的过滤类型 uriPermissionPatterns ,如果设置了过滤类型,则将需要申请权限的 Uri 与之匹配。匹配不同过,则抛出异常。 9. 检查调用者自己是否有权限访问这个 Uri 。如果没有,抛出异常。 10. 从 mGrantedUriPermissions 中取得 target uid 对应的 HashMap<Uri, UriPermission> 数据结构。用 target uid 和 Uri 生成 UriPermission 并保存在 mGrantedUriPermissions 中。 revokeUriPermission 实现分析。 找到该 Uri 对应的 ContentProvider ,然后删除 mGrantedUriPermissions 中与 Uri 对应的所有权限。 2.2 permission 的动态检查 这里的动态检查是指是 package 在程序运行过程中进行某些操作或者数据访问时才进行的 check ,与之对应的是应用程序安装或者升级时 PackageManagerService 通过扫描包中的静态权限信息相对应。 系统与权限 检查 相关的机制的实现主要集中在 PackageManagerService 和 ActivityManagerService 中。 ActivityManagerService 主要负责的是底层的 uid 层次的身份检查; PackageManagerService 则维护了 uid 到自己拥有的和被授予的权限的一张表。在通过 ActivityManagerService 的身份检查后, PackageManagerService 根据请求者的 uid 来查看这张表,判断其是否具有相应的权限。 除此之外, per-URI permission 机制的实现也需要一张表,它维护在 ActivityManagerService 中,它建立了从 content URI 到被授权访问这个 URI 的 component 之间的映射。但是它也需要借助 PackageManagerService 的机制来辅助实现。 2.2.1 framework 提供的接口 Android framework 中提供了一些接口用来对外来的访问(包括自己)进行权限检查 。 这些接口 主要通过 ContextWrapper 提供,具体实现在 ContextImpl 中 。如果 package 接受到外来访问者的操作请求,那么可以调用这些接口进行权限检查。一般情况下可以把这些接口的检查接口分为两种,一种是返回错误,另一种是抛出异常。 主要包含如下几组: n permission 和 uid 检查 API 下面这一组接口主要用来检查某个调用(或者是其它 package 或者是自己)是否拥有访问某个 permission 的权限。参数中 pid 和 uid 可以指定,如果没有指定,那么 framework 会通过 Binder 来获取调用者的 uid 和 pid 信息,加以填充。返回值为 PackageManager.PERMISSION_GRANTED 或者 PackageManager.PERMISSION_DENIED 。 public int checkPermission(String permission, int pid, int uid) // 检查某个 uid 和 pid 是否有 permission 权限 public int checkCallingPermission(String permission) // 检查调用者是否有 permission 权限,如果调用者是自己那么返回 PackageManager.PERMISSION_DENIED public int checkCallingOrSelfPermission(String permission) // 检查自己或者其它调用者是否有 permission 权限 下面这一组和上面类似,如果遇到检查不通过时,会抛出异常,打印消息 。 public void enforcePermission(String permission, int pid, int uid, String message) public void enforceCallingPermission(String permission, String message) public void enforceCallingOrSelfPermission(String permission, String message) n per-URI 检查 API 为某个 package 添加访问 content Uri 的读或者写权限。 public void grantUriPermission(String toPackage, Uri uri, int modeFlags) public void revokeUriPermission(Uri uri, int modeFlags) 检查某个 pid 和 uid 的 package 是否拥有 uri 的读写权限,返回值表示是否被 granted 。 public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) public int checkCallingUriPermission(Uri uri, int modeFlags) public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags) public int checkUriPermission(Uri uri, String readPermission,String writePermission, int pid, int uid, int modeFlags) 检查某个 pid 和 uid 的 package 是否拥有 uri 的读写权限,如果失败则抛出异常,打印消息 。 public void enforceUriPermission(Uri uri, int pid, int uid, int modeFlags, String message) public void enforceCallingUriPermission(Uri uri, int modeFlags, String message) public void enforceCallingOrSelfUriPermission(Uri uri, int modeFlags, String message) public void enforceUriPermission(Uri uri, String readPermission, String writePermission,int pid, int uid, int modeFlags, String message) 2.2.2 实现分析 ContextImpl.java 中提供的 API ,其实都是由 ActivityManagerService 中的如下几个接口进行的封装。 public int checkPermission(String permission, int pid, int uid) throws RemoteException; // 主要用于一般的 permission 检查 public int checkUriPermission(Uri uri, int pid, int uid, int mode) throws RemoteException; // 主要用于 Content Uri 的 permission 检查 n checkPermission 的实现分析 1. 如果传入的 permission 名称为 null ,那么返回 PackageManager.PERMISSION_DENIED 。 2. 判断调用者 uid 是否符合要求 。 1 ) 如果 uid 为 0 ,说明是 root 权限的进程,对权限不作控制。 2 ) 如果 uid 为 system server 进程的 uid ,说明是 system server ,对权限不作控制。 3 ) 如果是 ActivityManager 进程本身,对权限不作控制。 4 )如果调用者 uid 与参数传入的 req uid 不一致,那么返回 PackageManager.PERMISSION_DENIED 。 3. 如果通过 2 的检查后,再 调用 PackageManagerService.checkUidPermission ,判断 这个 uid 是否拥有相应的权限,分析如下 。 1 ) 首先它通过调用 getUserIdLP ,去 PackageManagerService.Setting.mUserIds 数组中,根据 uid 查找 uid (也就是 package )的权限列表。一旦找到,就表示有相应的权限。 2 ) 如果没有找到,那么再去 PackageManagerService.mSystemPermissions 中找。这些信息是启动时,从 /system/etc/permissions/platform.xml 中读取的。这里记录了一些系统级的应用的 uid 对应的 permission 。 3 )返回结果 。 n 同样 checkUriPermission 的实现 主要在 ActivityManagerService 中,分析如下: 1. 如果 uid 为 0 ,说明是 root 用户,那么不控制权限。 2. 否则,在 ActivityManagerService 维护的 mGrantedUriPermissions 这个表中查找这个 uid 是否含有这个权限,如果有再检查其请求的是读还是写权限。 3 Android 签名机制 关于签名机制,其实分两个阶段。 包扫描阶段需要进行完整性和证书的验证。普通 package 的签名和证书是必须要先经过验证的。具体做法是对 manifest 下面的几个文件进行完整性检查。完整性检查包括这个 jar 包中的所有文件。如果是系统 package 的话,只需要使用 AndroidMenifest.xml 这个文件去提取签名和验证信息就可以了。 在权限创建阶段。如果该 package 来自 system img (系统 app ),那么 trust it ,而且使用新的签名信息去替换就的信息。前提是如果这个 package 与其它 package 共享一个 uid ,那么这个共享 uid 对应的 sharedUser 中保存的签名与之不一致,那么签名验证失败。有些时候系卸载一个 app ,但是不删除数据,那么其 PackageSettings 信息会保留,其中会保存签名信息。这样再安装是就会出现不一致。 3.1 Android Package 签名原理 android 中系统和 app 都是需要签名的。可以自己通过 development/tools/make_key 来生成公钥和私钥。 android 源代码中提供了工具 ./out/host/linux-x86/framework/signapk.jar 来进行手动签名。签名的主要作用在于限制对于程序的修改仅限于同一来源。系统中主要有两个地方会检查。如果是程序升级的安装,则要检查新旧程序的签名证书是否一致,如果不一致则会安装失败;对于申请权限的 protectedlevel 为 signature 或者 signatureorsystem 的,会检查权限申请者和权限声明者的证书是否是一致的。签名相关文件可以从 apk 包中的 META-INF 目录下找到。 signapk.jar 的源代码在 build/tools/signapk ,签名主要有以下几步: l 将除去 CERT.RSA , CERT.SF , MANIFEST.MF 的所有文件生成 SHA1 签名 首先将除了 CERT.RSA , CERT.SF , MANIFEST.MF 之外的所有非目录文件分别用 SHA-1 计算摘要信息,然后使用 base64 进行编码,存入 MANIFEST.MF 中。 如果 MANIFEST.MF 不存在,则需要创建。存放格式是 entry name 以及对应的摘要 l 根据 之前计算的 SHA1 摘要信息,以及 私钥生成 一系列的 signature 并写入 CERT.SF 对 整个 MANIFEST.MF 进行 SHA1 计算,并将摘要信息存入 CERT.SF 中 。然后对之前计算的所有摘要信息使用 SHA1 再次计算数字签名,并写入 CERT.SF 中。 l 把公钥和签名信息写入 CERT.RST 把之前整个的签名输出文件 使用私有密钥计算签名。同时将签名结果,以及之前声称的公钥信息写入 CERT.RSA 中保存。 3.2 Package 的签名验证 安装时对一个 package 的签名验证的主要逻辑在 JarVerifier.java 文件的 verifyCertificate 函数中实现。 其主要的思路是通过提取 cert.rsa 中的证书和签名信息,获取签名算法等信息,然后按照之前对 apk 签名的方法进行计算,比较得到的签名和摘要信息与 apk 中保存的匹配。 第一步。提取证书信息,并对 cert.sf 进行完整性验证。 1. 先找到是否有 DSA 和 RSA 文件 ,如果找到则对其进行 decode ,然后读取其中的所有的证书列表(这些证书会被保存在 Package 信息中,供后续使用)。 2. 读取这个文件中的签名数据信息块列表,只取第一个签名数据块。读取其中的发布者和证书序列号。 3. 根据证书序列号,去匹配之前得到的所有证书,找到与之匹配的证书。 4. 从之前得到的签名数据块中读取签名算法和编码方式等信息 5. 读取 cert.sf 文件,并计算整个的签名,与数据块中的签名(编码格式的)进行比较,如果相同则完整性校验成功。 第二步。使用 cert.sf 中的摘要信息,验证 MANIFEST.MF 的完整性。 在 cert.sf 中提取 SHA1-Digest-Manifest 或者 SHA1-Digest 开头的签名 数据块 ( -Digest-Manifest 这个是整个 MANIFEST.MF 的摘要 信息,其它的是 jar 包中其它文件的摘要信息 ), 并逐个对这些数据块 进行验证。验证的方法是,现将 cert.sf 看做是很多的 entries ,每个 entries 包含了一些基本信息,如这个 entry 中使用的摘要算法( SHA1 等),对 jar 包中的哪个文件计算了摘要,摘要结果是什么。 处理时先找到每个摘要数据开中的文件信息,然后从 jar 包中读取,然后使用 -Digest 之前的摘要算法进行计算,如果计算结果与摘要数据块中保存的信息的相匹配,那么就完成验证。