how-to-root-android

如何实现Root管理(一)关于ROOT的几个问题

概述

本文将简单介绍CM里对Root权限的开放,关闭,对应用su授权管理进行介绍。内容将涉及init过程,System property的加载,AppOpsManager,Selinux的相关内容。

流程概述:

* 如何让usr版本拥有su的方法:在编译时把su打包到固件里

* 如何实现su的开关:我们期望在init.rc里添加一个service来实现开关su的权限。可以通过修改su.c,修改init相关

* 如何实现应用su的权限管理:修改AppOpsManager来进行管理

理解Root用户,UID和GID

在Linux里用户种类很简单,只有Root和非Root两种。每个进程都有自己的ID(UID),用户组ID(GID),而Root用户的UID和GID都是0。在Android里,每个应用是一个独立的用户运行在独立的虚拟机里,所以应用权限的大小是由它用户的身份决定的。

名称 功能
AID_ROOT 0 ROOT用户
AID_SYSTEM 1000 系统
AID_SHELL 2000 shell用户
AID_APP 10000 第一个app user

一般UID 10000以前都是系统预留用户ID,更多完整的定义可以查看 system/core/android_filesystem_config.h

为什么user版本的adb不能Root

我们可以先查看不同版本下adbd进程的信息,观察不同。

user版本下adb的运行信息

eng,userdebug下的adb

可以观察到在user版本下adb的用户是AID_SHELL,而在userdebug版本下adbd的用户是Root用户。所以这就导致了user版本下无法root。

Note,带root的shell的uid和gid虽然是0,但是它的安全上下文(context)u:r:shell:s0还是shell,所以从shell里生出的进程还是被shell的context所限制。之前android4.4的时候,selinux没有开启的时候。你如果从shell生出来,你就是root用户组了可以胡作非为了。

为什么现在android要root变得艰难

Android 4.3以后,system分区被挂起为nosuid,任何zygote生出来的进程都不能进行setuid的操作。Selinux在Android 5.0后默认开启为enforce模式了,很多操作都受到限制,在selinux打开的情况下修改root权限,用寸步难行来形容真不为过。

su.c

su可以简单的理解为一个类似命令行的程序,它的存放路径为android/platform/system/extras/su ,在编译的时候这个su会被编译为二进制文件存储在系统镜像里system/xbin下,这个文件只有在userdebug,eng下才会编译到系统镜像里,所以user版本的固件是不能调用su。

int main(int argc, char** argv) {
        uid_t current_uid = getuid();
        // 对调用su的身份做检查
        if (current_uid != AID_ROOT && current_uid != AID_SHELL) 
            error(1, 0, "not allowed");
        // 省略...
}

这也是其他进程无法调用su的原因,如果注释掉这一段su就不会对用户id进行检查了(风险很大这样做)。

如何打包su.c进入usr版本

观察su文件夹下的mk文件

发现大量debug的tag,所以在user版本下它不会被打包到固件里。需要把整个TAG的值改为optional。不过我记得当初我去掉以后还是没有打包进去,估计是还有其他地方还有限制。我是修改了产品的mk保证在编译时一定编译这个模块才行的

PRODUCT_PACKAGES += \
    su

Fun facts with HTTP

当输入一个网址后的事….

HTTP是一种不保存状态的协议(stateless)

HTTP协议自身不对请求和响应之间的通信协议状态进行保存。这样的设计是为了保证协议的简单高效。

Cookie

但是随着Web的发展,越来越多的需求需要记录状态,于是引入了Cookies的设计。Cookie技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。Cookie就像是某种token,客户端一份,服务器一份,下次通信时再互相验证。

HTTP管线化(PipeLine)

HTTP是基于TCP的应用嘛,所以早期的HTTP每进行一次HTTP通信,就开断一次TCP连接。我们知道TCP要经过三次握手才能通信。随着web的复杂,这样频繁开断很浪费资源。针对这个问题于是提出了HTTP keep-live方法,其特点是如果任意一端没有明确提出断开连接,则保持TCP连接状态。并且从前必须等到上一个请求响应才能发起下一个请求,而又了管线化技术可以连续请求啦。

压缩传输技术

传送数据压缩才能更快的传输啊,android 2.3以后对HttpUrlConnection进行了gzip压缩的支持

 

 

参考

《图解HTTP》作者: 【日】上野宣 出版社: 人民邮电出版社

 

Volley源码分析

引言

Volley是Google在2013I/O大会发布,如其名它简洁高效,之后几乎所有人都在用它。今天我们来分析一下它。

整体结构及其策略

RequestQueue:请求队列,所有的网络请求将在这PriorityQueue处理。

Dispatcher :负责请求的分发,一个负责发起缓存请求,一个负责发起网络请求

HttpClientStack和HurlStack :是真正发起网络请求的地方,这里是针对不同的Android版本分别对AndroidHttpClient(已废弃,适用于API 9以下)和HttpUrlConnection的封装

策略:

请求加入到RequestQueue里,然后被CacheDispatcher取出请求检查是否在Cache里,如果在取出数据,传回给主线程ok结束。如果Cache没有命中或者cache过期就把请求放入网络请求队列里,然后发起网络请求结束。

源码详细分析策略

分析源码当然要从我们平时的使用方法作为入口开始入手

java

Volley.newRequestQueue(Context context)

 

调用这个静态方法最后会掉到(省略部分代码)

 public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

      // 根据不同Android版本新建不同的网络请求处理栈
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);
        // 新建RequestQueue,并调用start方法
        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
            queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
            queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        queue.start();

        return queue;
    }

跟着上面RequestQueue的start方法

  public void start() {
        stop();  // 确保一批Dispatcher在活动
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // 根据Dispatcher池子大小来创造Dispatcher
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

可以看到Dispatcher在RequestQueue调用start方法的时候被创建,且在这个时候分别开启他们的线程。因为Dispatcher实际上是Thread,所以start后我们直接分析它的run方法

CacheDispatcher的run方法(代码有所省略):

  @Override
    public void run() {
    // 这里设置线程的优先级很重要,后面会说明
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        Request<?> request;
        while (true) {
           // ...
            try {
                // 尝试从队列里取出一个请求
                request = mCacheQueue.take();
            } catch (InterruptedException e) {
                if (mQuit) {
                    return;
                }
                continue;
            }
            try {
                // 如果Request被取消,直接结束
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // 尝试读取缓存
                Cache.Entry entry = mCache.get(request.getCacheKey());
                // 缓存为空,加入网络请求队列
                if (entry == null) {
                    request.addMarker("cache-miss");
                    mNetworkQueue.put(request);
                    continue;
                }

                // 缓存过期,也加入网络请求队列
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // Cache命中,读取缓存
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // 不需要刷新的叫完全命中,直接投递结果
                    mDelivery.postResponse(request, response);
                } else {
                    // 有缓存但是需要刷新的叫Soft-expired cache hit,我们投递结果,但是也把请求加入网络请求队列
                    // ...
                 }
        }
    }

如何传回数据到主线程呢?很简单就是有一个持有主线程Looper的Handler了就行啦。这个类是ExecutorDelivery,就不分析了。

Volley ImageLoader 分析

要分析ImageLoader先要提前说一下其中的一个数据封装类ImageContainer它持有关于网络图片的所有信息:

public class ImageContainer {

        private Bitmap mBitmap;
        // 图片请求结果的listener
        private final ImageListener mListener;

        // 图片请求缓存key
        private final String mCacheKey;

        private final String mRequestUrl;
}

这个ImageLoader设计最棒的地方,不仅在于可以读取缓存图片,还可以对重复的请求进行过滤避免发起多次重复的请求(这种重复请求的情况,在Listview有图片时上下滚动列表时尤为明显)

那么它是如何实现这一功能的呢?答案也很简单它用了一个Map来对请求作缓存。

// InFlightRequest: 这个Map用来缓存还正在请求中的请求队列,用这个队列来实现避免重复请求,当请求结束后移除请求
    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
            new HashMap<String, BatchedImageRequest>();

下面我们来看ImageLoader的get方法,通常我们来获取图片的方法(代码有所省略):

    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight, ScaleType scaleType) {

        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

        // 查看是否有缓存
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // 返回缓存数据
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        // 没有缓存,准备请求
        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        // 检查一下请求是否已经请求已经在处理
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // 如果已经在了,我们只在这个请求中添加我们的监听器
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // 请求没有正在处理,加入网络请求队列,别再InFlight里记录
        Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
                cacheKey);

        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }

小知识

BlockingQueue

它与一般的Queue不同的是多了对两个操作的支持,等到队列不为空的时候取元素,和等队列不为全满的时候放入元素。(中文好拗口,看英文)A Queue that additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.

Android 自己使用线程时的注意事项

一个重要的知识点是,当一个线程被创建的时候,它将拥有和同一组线程里同样的优先级。

这意味着入股,你在UI thread里打开一个线程,如果不设置恰当的优先级,这个worker thread将和UI thread享受同样的优先级,这样可能导致worker thread和UI thread抢资源导致卡顿。前面你也可能注意到了,在Volley新建Dispatcher的时候是在UI thread,所以它把线程的优先级设置为了THREAD_PRIORITY_BACKGROUND。

android里设置priority是从负数到正数的,优先级递减的。如何理解记忆呢,可以想成这个priority的值表达了这个线程对他人的友好程度,友好程度越低代表它的时间越珍贵。

正常情况下我们希望是这样的

(在AsyncTask里和IntentService里默认是这样实现的)

参考

* [Google I/O 2013 - Volley: Easy, Fast Networking for Android](https://www.youtube.com/watch?v=yhv8l9F44qo)
*
* [Volley 源码解析](http://codekk.com/blogs/detail/54cfab086c4761e5001b2542)
* [BlockingQueue java doc](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html)

ArrayMap VS HashMap

在Java里面用Collection里面的HashMap作为容器我们使用的频率很高,而ArrayMap是Android api提供的一种用来提升特定场和内存使用率的特殊数据结构。今天我就写一篇博客记录一下。

HashMap

QQ截图20150823235946

Java库里的HashMap其实是一个连续的链表数组,通过让key计算hash值后插入对应的index里。当hash值发生碰撞时,可以采用线性探测,二次hash,或者后面直接变成链表的结构来避免碰撞。因为hash的值不是连续的,所以hashmap实际需要占用的大小会比它实际能装的item的容量要大。我们可以看一下HashMap的源码:

  
 public HashMap(int initialCapacity, float loadFactor)   
 {   
     // 初始容量不能为负数  
     if (initialCapacity < 0)   
         throw new IllegalArgumentException(   
        "Illegal initial capacity: " +   
             initialCapacity);   
     // 如果初始容量大于最大容量,让出示容量  
     if (initialCapacity > MAXIMUM_CAPACITY)   
         initialCapacity = MAXIMUM_CAPACITY;   
     // 负载因子必须大于 0 的数值  
     if (loadFactor <= 0 || Float.isNaN(loadFactor))   
         throw new IllegalArgumentException(   
         loadFactor);   
    //....
    // 设置容量极限等于容量 * 负载因子  
     threshold = (int)(capacity * loadFactor);   
     // 初始化 HashMap用于存储的数组  
     table = new Entry[capacity];            // ①  
     init();   
 }   

你会发现它又一个变量叫loadfactor,还有threshold。threshold就是临界值的意思,代表当前HashMap的储存机构能容纳的最大容量,它等于loadfactor * 容量。当HashMap记录存入的item size大于threshold后,HashMap就会进行扩容(resize)。当我们第一次新建一个HashMap对象的时候,默认的容量是16,若你只打算在HashMap里放入3个元素那将浪费至少13个空间。

ArrayMap

ArrayMap是怎么实现节省内存的呢?先放数据结构图:

QQ截图20150824001700

 

他用两个数组来模拟Map,第一个数组存放存放item的hash值,第二数组是把key,value连续的存放在数组里,通过先算hash在第一个数组里找到它的hash index,根据这个index在去第二个数组里找到这个key-value。

在这里,在第一个数组里查找hash index的方法当然是用二分查找啦(binary search)。QQ截图20150824002101

这个数据结构的设计就做到了,有多个item我就分配多少内存,做到了memory的节约。并且因为数据结构是通过数组组织的,所以遍历的时候可以用index直接遍历也是很方便的有没有!但是缺点也很明显,查找达不到HashMap O(1)的查找时间。

当要存储的对象较少的时候(1000以下的时候)可以考虑用ArrayMap来减少内存的占用。

曲线的魔力—贝塞尔曲线(二)

先放效果图!(类似腾讯qq的消息消失动画)上一篇回顾

 

gooey怕了吧,有没有好奇这个动画到底是怎么做的?其实原理超级简单,不要被唬住了。这里就是展现贝塞尔曲线牛逼的时候到了。其实上一篇将贝塞尔曲线只是说了它的原理,但是没有一个形象的比喻,下面我要用一个形象的比喻来解释贝塞尔曲线。控制贝塞尔曲线的过程其实就像是操纵橡皮筋,我们首先把橡皮筋的两头用钉子固定住(这就是确定了start point和end point),然后我们可以用手去拨动橡皮筋中间的部分,这就是贝塞尔曲线另外一个概念控制点(control point),我们可以通过设置不同的控制点产生不同的曲线。cubic

explain

看图其实这个动画就是两个圆形,中间两条曲线由贝塞尔曲线画出来!是不是很简单!但是难点在于,如何控制两条曲线的变化,也就是如何确定贝塞尔曲线的控制点呢?我在这里的实现比较简单,先看下图:

explain

 

控制点如图就是p5和p6,他们分别是线p1p4和p2p3线的中点。每次动态更具圆的位置动态计算控制点的位置就ok啦!

代码我就不讲解啦,很简单:

package com.example.qianlv.gooeyeffect;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.Transformation;

/**
 * Created by qianlv on 2015/8/16.
 */
public class GooeyView extends View {
    float radius1;
    float radius2;
    RectF rect1;
    RectF rect2;
    Paint paint;
    TimeAnimation animation;
    private float mInterpolatedTime;
    PointF p1;
    PointF p2;
    PointF p3;
    PointF p4;
    PointF p5;
    PointF p6;
    float dx1;
    float dy1;
    float dx2;
    float dy2;

    public GooeyView(Context context) {
        super(context);

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(0xff30accf);
        paint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        // 设置两个圆大小
        radius1 = width * 0.13f;
        radius2 = width * 0.1f;

        rect1 = new RectF();
        rect1.left = width*0.05f;
        rect1.top = rect1.left;
        rect1.right = radius1 * 2;
        rect1.bottom = rect1.top + radius1 * 2;

        rect2 = new RectF();
        rect2.left = rect1.right + 200;
        rect2.right = rect2.left + radius2 * 2;
        rect2.top = rect1.centerY() - radius2;
        rect2.bottom = rect2.top + radius2 * 2;
        dx1 = radius1*0.5f;
        dy1 = dx1*1.732f;
        dx2 = radius2*0.5f;
        dy2 = dx2*1.732f;

        p1 = new PointF(rect1.centerX()+dx1,rect1.centerY()-dy1);
        p3 = new PointF(rect1.centerX()+dx1,rect1.centerY()+dy1);
        p2 = new PointF(rect2.centerX()-dx2,rect2.centerY()-dy2);
        p4 = new PointF(p2.x,rect2.centerY()+dy2);
        p5 = new PointF((p1.x + p4.x)*0.5f,(p1.y + p4.y)*0.5f);
        p6 = new PointF((p2.x+p3.x)*0.5f,(p2.y+p3.y)*0.5f);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRGB(255, 255, 255);
        canvas.drawCircle(rect1.centerX(), rect1.centerY(), radius1, paint);
        canvas.drawCircle(rect2.centerX(), rect2.centerY(), radius2, paint);

        // 绘制贝塞尔曲线
        Path curve1 = new Path();
        curve1.moveTo(p1.x,p1.y);
        curve1.quadTo(p5.x,p5.y,p2.x,p2.y);
        curve1.lineTo(p4.x,p4.y);
        curve1.quadTo(p6.x,p6.y,p3.x,p3.y);
        curve1.lineTo(p1.x, p1.y);
        canvas.drawPath(curve1,paint);

        // 每次重绘时移动距离
        rect2.left +=5;
        rect2.right +=5;
        if (rect2.right > getWidth()){
            rect2.left = rect1.right + 30;
            rect2.right = rect2.left + radius2 * 2;
        }

        // 重新计算每个点的位置
        p2.x = rect2.centerX()-dx2;
        p2.y = rect2.centerY()-dy2;
        p4.x = p2.x;
        p4.y = rect2.centerY()+dy2;
        p5.x = (p1.x + p4.x)*0.5f;
        p5.y = (p1.y + p4.y)*0.5f;
        p6.x = (p2.x+p3.x)*0.5f;
        p6.y = (p2.y+p3.y)*0.5f;
    }

    // 用于改变时间
    private class TimeAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            mInterpolatedTime = interpolatedTime;
            invalidate();
        }
    }

    private void startAnimation() {
        animation = new TimeAnimation();
        animation.setDuration(10000);
        animation.setInterpolator(new LinearInterpolator());
        animation.setRepeatCount(Animation.INFINITE);
        animation.setRepeatMode(Animation.REVERSE);
        startAnimation(animation);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimation();
    }
    private void stopAnimation() {
        this.clearAnimation();
        postInvalidate();
    }
    @Override
    protected void onDetachedFromWindow() {
        stopAnimation();
        super.onDetachedFromWindow();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 重置
        rect2.left = rect1.right + 30;
        rect2.right = rect2.left + radius2 * 2;
        startAnimation();
        return true;
    }
}

参考:

 

 

实习学习记录(一)

偷懒一下,打算写一个小系列记录实习期间学习到的知识点总结一下。

View绘制的流程

View和Activity一样其实也有自己的生命周期(这里的生命周期不是指像Activity一样有onPause,onResume这种一样,这里指代的是View的一辈子所要干的事的一种抽象概念)。不过View的生命周期远比Activity的生命周期简单。View的生命周期里主要做了三件事:Attachment/Detachment,遍历(英文是Traversal,View的树形结构遍历,里面包括了onMeasure,onLayout,onDraw等方法的递归遍历调用),保存恢复数据 。

关于Attachment:就像Fragment要依附于Activity一样它又一个onAttach的方法一样。View也有一个叫onAttachedToWindow的方法。在这个方法里面,我们一般进行资源分配,注册监听器。如果你看ListView的源代码你可以发现,google在这里对Adapter注册了监听器用于监听数据变化。对应的View还有onDetachedWindow,在这里一般把分配的资源释放掉,还有把监听器弄掉。

关于遍历:

View在你看到前系统会对它走如下的流程:

Animate->Measure->Layout->Draw

Measure主要是要确定自己到底要占多少位置,多少大小。比如你要知道match_parent和wrap_content的时候你到底需要多大。Layout就是需要确定 child view的显示位置。Draw就是对view进行绘制。

Measure里新手第一次见到的时候会被好多新东西吓到。我来一个个解说,你先是会发现有一个MeasureSpec的数据结构,这个是对View的宽高,还有测量模式的一个结构封装。可以用MeasureSpec.getSize,MeasureSpec.getMode来进行调用。这里的Mode有三种模式:

  • Exactly:比如你确切的给view一个大小如200dp,还有match_parent,LinerLayout里的weight这种都属于Exactly确切模式
  • AT_MOST:这里是Wrap_content的应用。
  • UNSPECIFIED:不确定模式,一般是在ListView和ScrollView这种内容长度不确定的东西。

这里其实我们再自己自定义控件的时候我们没必要考虑每一种模式的计算情况,因为毕竟是自己使用,你可以假定我们永远只用match_parent啊或者是永远wrap_content,所以你主需要写一种模式的测量方法即可。(当然除非你想确保你的控件的普遍适用性,你可以考虑每一种模式)

onLayout

这里是利用前面测量好的数据(getMeasureWidth和getMeasureHeight)来确认view里的child view的放置位置。这里要注意有意思的一点是,View.getWidth和View.getHeight只有在onLayout完之后才可以使用,不然在之前调用返回的都是0.(这种返回0的情况我想很多人在写自己自定义的ViewGroup还有在自己自定义的Adapter的时候都会遇到)那是不是在view显示过一次之前我们都无法获得view的宽高了呢?并不是呢,我们这里还有一个利器叫onPreDrawListener,这个监听器的触发时间将在onLayout之后和onDraw之前。

Draw:这里面用得最多得就是invalidate。和requestLayout类似,调用它以后会触发View树的遍历,就是对它的parent一直进行递归调用直到到了view root。这里注意invalidate是我在UI线程里调用的函数,如果我在非UI线程的时候需要调用postInvalidate。

状态保存:一般是通过Override,onSaveInstanceState和onRestoreInstanceState实现。

clipRect的使用:如果每次调用invalidate的时候都把整个view重绘,将是很耗性能的事情。我们可以通过规定每次需要重绘的区域(一个矩形Rect)让GPU知道每次我只需要对View的这部分显示进行更新就好。如下面这个例子,在这个自定义View包括了文字还有一个按钮,按钮包括一个进度条显示的功能。若果每次只需要更新的是按钮的进度,而重绘整个view就显得有点浪费。可以我们可以选择每次只更新那个按钮的位置。通过设置canvas.clipRect(Rect clipArea)即可。clip

StaticLayout用于在canvas里绘制文字

这个相当于隐藏的API,在canvas里我们绘文字的时候,drawText方法字符串有多长它就画多长,它不会自动换行。用StaticLayout可以解决这个问题。通过建立一个StaticLayout区域,在里面drawText是会自动换行的。TextView就是基于这个实现的。想实现drawText文字换行还有一招,只是有点麻烦思路如下:获取显示区域大小如DisplayWidth,然后测量用画笔画所需字符串将得到的宽度TextWidth(这里是通过调用Paint.getTextBounds(),或Paint。getTextWidths获得,区别是前者是返回int后者是float。后者更精准)如果TextWidth > DisplayWidth我们就对要显示的字符串进行substring然后在下一行的位置进行显示(下一行的位置也是我们自己计算的,用前面得到的TextBounds来获得getHeight)比较麻烦。代码我就不放了,实现效果大概如下:

QQ图片20150726112820

bezier

曲线的魔力——初识贝塞尔曲线

为什么我们需要贝塞尔曲线?

设想一下平时曲线对计算机来说是画出来的呢。无非就是要用一个函数y = f(x)来绘制曲线。但是我们能拿到手就用的函数曲线无非是sin,cos这种函数了,若我们想要更复杂的曲线,获取它的方程将会是很难的。

curve

如何产生这个曲线的方程呢?

这个时候,贝塞尔曲线的作用就来了。

什么是贝塞尔曲线?

贝塞尔曲线(英语:Bézier curve)是电脑图形学中相当重要的参数曲线。是用几个很少的控制点就可以产生平滑复杂的曲线的方法。我们经常在开发中用贝塞尔曲线来绘制曲线,或者控制动画的时间变化(擦除函数)。

下面展示一下贝塞尔曲线算法的绘制过程:

先定义三个点

贝塞尔1

 

在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。

贝塞尔2

 

根据上一步得到的比例,从第二条线段上找出对应的点 E,使得 AD:AB = BE:BC

贝塞尔3

 

连接D,E得

贝塞尔4

 

从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE = AD:AB = BE:BC。

贝塞尔6如果把这个过程反复的做下去将形成一个曲线!

贝塞尔5

 

wiki上的动图效果如下:(好像要点开才看得到)

Bézier_2_big

 

当然控制点还可以有更多来操作出更复杂的曲线。

在Android里的应用

Android里的Path提供了画贝塞尔曲线的API,下面我来做一个简单介绍。

Path包含贝塞尔曲线的API主要包括,quadTo,cubicTo,rCubicTo,rQuadTo。

其中quadTo是有两个控制点的贝赛尔曲线,cubicTo有三个控制点。

例子:

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

/**
 * Created by Jiaqi Ning on 5/7/2015.
 */
public class PathView extends View {
    Paint paint;

    public PathView(Context context) {
        super(context);

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(0xff41be9d);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    //draw the curve
        Path path = new Path();
        path.moveTo(50,50);
        path.cubicTo(50,50,50,300,250,300);
        canvas.drawPath(path,paint);
// draw the control line
        path.reset();
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(2);
        path.moveTo(50,50);
        path.lineTo(50,300);
        path.lineTo(250,300);
        canvas.drawPath(path,paint);

    }
}

curve_screenshot更强大的用法,考完试再写吧 Orz…

参考:

 

 

Java里的异常

 

前言:最近都在准备考试复习都木有这么学习新姿势,临时找了以前看书的记录凑出了这个文章,太罪过了><

感觉自己目前写的程序都是编译器在强制自己要做异常处理,对于怎么样编写优秀的异常处理程序还不算很清楚,下面先整理一下Thinking in java里看到的关于异常的知识吧。异常处理的原则是”只有你知道如何处理的情况下才捕获异常”,其中一个重要目标是将错误处理代码和错误发生地点分离。

分类:分为被检查异常(checked)和不被检查异常(unchecked)。这里的检查指的是否进行编译时期的强制检查。

重新抛出对象fillStackTrace的用法;异常出现后异常对象会被压栈,我们可以通过打印栈里的信息,来获取发生异常的函数调用情况,栈底的元素是调用序列里的第一个方法。这里要注意异常重新抛出的一个问题,有时候我们捕获一个异常后我们并不希望在这处理,只是想像上一层再次抛出,比如像catch(Exception e ) {

throw e; // rethrow the exception

}这样重新抛出的话,若用printStackTrace打印栈元素的话,只会显示最原始抛出异常点的信息而不包括重新抛出点的位置,若要把重新抛出异常的信息也要加入栈里,需要调用fillInStackTrace这个方法把信息压入。

finally的注意和特别用法。finally作为try/catch机制里最后进行处理的措施,就算try里遇到了break和return,finally也还是会执行的。

继承对异常的影响。若子类继承了父类的某个方法,子类可以不抛出父类规定的异常。子类要覆写(Override)父类的某个方法,若原方法定义了某个异常,子类的方法里不能再给这个方法抛出更多异常。

切勿“吞食”掉异常。写异常有时候的确很烦,特别遇到有很多异常嵌套的时候,我们为了应付编译器就只写了一个try/catch在那或者简单的throw出去,这个异常就被吞食了,这不是一个好习惯。最次我们也要把异常打印出来提醒,或者我不知道这里遇到异常应该怎么处理,我就抛出一个RuntimeException结束程序把。

新玩具:Android里的DataBinding

为什么我们需要Data Binding

传统的MVC模式如下图所示:

mvc这种模式的初衷是让业务逻辑和View分开,让我们在修改界面而不改变业务逻辑的时候更简单,但是实习操作的时候往往很难完全对View和Controller或Model很好的分离。并且三种组件也增加了一定的复杂度。而当我们有了Data Binding技术我们就可以使用另外一种模式(这种模式常用于Windows软件开发)叫MVVM 全称是Model-View-ViewModel,Model代表的是你的业务逻辑,View是展示的视图,ViewModel是把二者绑定起来的接口。这样说还是有点抽象,在一般Android开发里,如果某个View展示的数据出现了变化,我们需要findViewById找到这个View,并且设置新更新的数值。而在MVVC里,因为Model和View是绑定的,如果model层的数据发送了变化,会自动通知更新view应该如何重新变化展示。听起来是不是非常棒哈。

 如何在Android里使用

  • 准备环境

在整个工程里的build.gradle里添加如下依赖:

  dependencies {
       classpath "com.android.tools.build:gradle:1.3.0-beta1"
       classpath "com.android.databinding:dataBinder:1.0-rc0"
   }

在你需要的使用data binding的module对应的build.gradle里添加

apply plugin: 'com.android.databinding'

ps:这里需要Android Studio是1.3以上,gradle版本在1.3以上,我之前即使是这样设置了还是有问题,我把build tool还有兼容包全部升级到最新版本就木有问题了。

下面我将已一个例子来介绍Android的DataBinding,这个小程序叫MagicNumber,app里显示一个数从1开始,有一个按钮,按一次按钮数字加1,若数字是奇数数字将显示黄色,若是偶数显示蓝色。

 

 

evenodd

先写布局:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="num" type="com.lvable.databindingtest.MagicNumber"/>
    </data>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:text="The magic number"
            android:id="@+id/textView"
            android:layout_marginTop="43dp"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"/>

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="67dp"
            android:textSize="60sp"
            android:layout_below="@+id/textView"
            android:layout_centerHorizontal="true"
            android:text="@{num.num}"
            android:textColor="@{num.odd ? @color/light_yellow : @color/light_blue}"
            />

        <Button
            android:text="Add"
            android:id="@+id/btn"
            android:onClick="addCount"
            android:layout_below="@id/textView2"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="20dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </RelativeLayout>

</layout>

注意如下变化:布局文件里的根元素不再是ViewGroup而是一个叫layout的标签,这个标签里包含一个data标签还有一个View,data标签里面包含了要绑定的数据元素类型申明等信息。在本例子里,我们要绑定的数据类型叫MagicNumber,然后在布局里它的名字叫num,type是这个类的完整路径名(若是Java自带的类型直接写就好,比如String,int)。在其他地方若要用到这个绑定数据的值只需要用@ {obj.xxxx}就行。这里要注意一个问题,就是在访问obj的成员变量的时候不一定总是它的全名,我试了一下我的MagicNumber里有一个成员变量叫isOdd,但是要访问它的话我要这样写 @{num.odd}。在这里还可以使用一些语句来实现简单的逻辑操作,比如我在布局文件里写入了奇偶不同颜色不同的逻辑。在textView里

android:textColor="@{num.odd ? @color/light_yellow : @color/light_blue}"

带Observable模式的MagicNumber类

Observable模式就是,当绑定的数据发送变化的时候,这个绑定的View会自己更新显示信息。这里可以使用MagicNumber继承一个叫BaseObservable的类来实现。(也可以使用ObservableField来实现对某一个field的Observe而不用继承)。

package com.lvable.databindingtest;

import android.databinding.BaseObservable;
import android.databinding.Bindable;

import com.lvable.databindingtest.BR;


/**
 * Created by Jiaqi Ning on 21/6/2015.
 */
public class MagicNumber extends BaseObservable {
    private String num;
    private boolean isOdd;
    
    public MagicNumber(int num){
        this.num = Integer.toString(num);
    }
   
    @Bindable
    public String getNum() {
        return num;
    }
   
    @Bindable
    public boolean isOdd() {
        return isOdd;
    }
    
    public void addOne(){
        int c = Integer.parseInt(num);
        num = Integer.toString(++c);
        updateEven();
        notifyPropertyChanged(BR.num);
        notifyPropertyChanged(BR.odd);
    }

    private void updateEven(){
        int c = Integer.parseInt(num);
        isOdd = ((c % 2) != 0) ? true : false;
    }
}

这里要注意,需要有更新通知功能的成员变量的get方法里,添加@Bindable这个注解,然后在起更新数据的地方调用对应的notifyPropertyChanged(BR.xxx)。在本程序里是如果MagicNumber调用了addOne就通知数据更新。

(注:BR 是编译阶段生成的一个类,功能与 R.java 类似,用 @Bindable 标记过 getter 方法会在 BR 中生成一个 entry,当我们通过代码可以看出,当数据发生变化时还是需要手动发出通知。)

好了,终于到了最后真正实现绑定的一步了!

public class MainActivity extends AppCompatActivity {
    MagicNumber magicNumber = new MagicNumber(1);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setNum(magicNumber);
    }

    public void addCount(View view){
        magicNumber.addOne();
    }

}

注意,这里不再使用setContentView(),使用的DataBindingUtil.setContentView()返回一个binding,用它来绑定数据。(这里的set方法是更具之前data里写入的type动态生成的)。

还有一个更好用的特性,若你在布局文件里给了某个View一个ID,在Activity里你想要使用这个View你不再需要些findViewById,你只需要从前面绑定产生的那个binding.xxxx (xxx是view的id名)就可以直接对这个View进行操作了!Awesome!

参考:    Google官方guide

很好的binding demo教程

 

初识Java里RTTI和反射(二)

今天才突然发现,RTTI其实主要是C++里的说法和反射其实表达的意思差不多(反射主要是Java里对RTTI的一种实现),有一个主要的区别是,对RTTI来说,编译器会在编译期打开和检查class文件。但对“反射”来说,class文件在编译期间是不可使用的,而是由运行时环境打开和检查。

动机

一开始觉得动态获取某个类的类型信息好像觉得平时根本用不上,那到底反射他的使用场景是什么呢?我了解的场景有下:

  • 插件化构件的程序就会需要到这个强大的技术了。也就是新加的插件,本来不在原程序里,新的插件是从网络上下载下来,这个类不在原来的程序空间里。
  • 远程方法调用(RMI),它允许一个Java程序将对象分布到多台机器上。例如,你可能正在执行一项需要大量计算的任务,为了提高运算速度,想将计算划分到许多小的计算单元上。
  • 在Android里扩充Framework。如果厂家需要修改Framework,要在里面的某类里添加使用自定义的Jar包,使用该自定义Jar包的方法是用import,但是为保存和原生Framework的兼容性,对原生Framework的最少修改,可以使用ClassLoader动态加载自定义Jar包。

Reflect常用类方法介

  • Method

可以在获取某个Class对象包含的所有方法getMethods(),也可用getMethod(String name,Class<?>… parameterTypes)来获取特定的方法,例子如下:

Class clzz = Class.forName("DummyClass");
Method methods = clzz.getMethods();
for(Metho m : methos){
    m.toString();
}
// get the function name is "display",and the param type is Stirng
Method action = clzz.getMethod("display",String.TYPE);
// call this function
action.invoke("test");
  • getConstruct

getConstructs返回所有构造函数,getConstructor(Class<?>… parameterTypes)返回特定参数的构造函数。

 

考虑过几天敲一下,一个Android插件化构架的例子 : )