Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

系统截屏源码浅析

$
0
0

android中实现截屏的方式有很多种,形如下面几种:

1、通过view.getDrawingCache获取屏幕的图像数据,这也是众多开发同行朋友经常使用的一种方式,可惜的是这种方式并不适用于surfaceview。

2、利用adb命令,adb shell screencap -p path,再利用runtime去执行,但是这种方式需要获得系统权限方可。

3、通过framebuffer实现截屏,帧缓冲(framebuffer)是Linux为显示设备提供的一个接口,允许上层应用程序在图形模式下直接对显示缓冲区进行读写等操,这些都是由Framebuffer设备驱动来完成的。android中的framebuffer数据是存放在 /dev/graphics/fb0 文件中的,所以只要获取到framebuffer中的数据再转换成图片就实现截屏的功能啦,这不是半本片文章的重点介绍内容,这个后面或许会最为一个章节共享个大家。

4、 利用系统TakeScreenShotService截图。android设备可以通过电源键+音量下键可以实现截屏,很多手机设备上用手下拉状态栏也有截屏的选项,都是使用TakeScreenShotService截屏的,本文要介绍的是如何通过TakeScreenShotService实现截屏。

TakeScreenShotService源码分析,源码位于
frameworks\base\packages\SystemUI\src\com\android\systemui\screenshot\TakeScreenshotService.java
瞧瞧manifest文件先:

<service android:name=".screenshot.TakeScreenshotService"
     android:process=":screenshot"
     android:exported="false" />

这个service是设置了exported属性的,如果设置为true,则能够被调用或交互,否则不能。设置为false时,只有同一个应用程序的组件或带有相同用户ID的应用程序才能启动或绑定该服务。然而TakeScreenshotService所在应用程序的id是android.uid.systemui,所以一般的应用程序是没办法做到这一点的。

public class TakeScreenshotService extends Service {
    private static final String TAG = "TakeScreenshotService";
    private static GlobalScreenshot mScreenshot;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case 1:
                final Messenger callback = msg.replyTo;
                if (mScreenshot == null) {
                    mScreenshot = new GlobalScreenshot(TakeScreenshotService.this);
                }
                mScreenshot.takeScreenshot(new Runnable() {
                    @Override
                    public void run() {
                        Message reply = Message.obtain(null, 1);
                        try {
                            callback.send(reply);
                        } catch (RemoteException e) {
                        }
                    }
                }, msg.arg1 > 0, msg.arg2 > 0);
            }
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return new Messenger(mHandler).getBinder();
    }
}

TakeScreenshotService 源码就这么多,可以很清晰的看见截屏的功能是由mScreenshot.takeScreenshot实现的。

void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
    .....
}

finisher是在截屏之后的回调,谁发起的截屏在截屏完成之后就需要告诉需要者已经完成了。第二个和第三个就是截屏时是否显示状态栏和导航栏。

上面也提到了手机上截屏在状态栏下拉时通常有个选项,所以我们就移步到PhoneStatusBar.java瞧瞧。
源码路径:frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\PhoneStatusBar.java

public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
    .......
    .......
    private void takeScreenshot() {
        // 截屏图片存放位置
        String imageDir = Settings.System.getString(mContext.getContentResolver(), Settings.System.SCREENSHOT_LOCATION);
        File file = new File(imageDir + UserHandle.myUserId() + "/Screenshots");
        String text = null;
        Log.e(">>>>>>", "imageDir=" + imageDir);
        file.mkdir();
        if (!file.exists()) {
            if (imageDir.equals("/mnt/sdcard")) {
                text = mContext.getResources().getString(R.string.sdcard_unmount);
            } else if (imageDir.equals("/mnt/external_sd")) {
                text = mContext.getResources().getString(R.string.external_sd_unmount);
            } else if (imageDir.equals("/mnt/usb_storage")) {
                text = mContext.getResources().getString(R.string.usb_storage_unmount);
            }
            Toast.makeText(mContext, text, 3000).show();
            return;
        }
        synchronized (mScreenshotLock) {
            if (mScreenshotConnection != null) {
                return;
            }
            // 在这里绑定了截屏的TakeScreenshotService
            ComponentName cn = new ComponentName("com.android.systemui",
                    "com.android.systemui.screenshot.TakeScreenshotService");
            Intent intent = new Intent();
            intent.setComponent(cn);
            ServiceConnection conn = new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    synchronized (mScreenshotLock) {
                        if (mScreenshotConnection != this) {
                            return;
                        }
                        Messenger messenger = new Messenger(service);
                        Message msg = Message.obtain(null, 1);
                        final ServiceConnection myConn = this;
                        Handler h = new Handler(mHandler.getLooper()) {
                            @Override
                            public void handleMessage(Message msg) {
                                synchronized (mScreenshotLock) {
                                    if (mScreenshotConnection == myConn) {
                                        mContext.unbindService(mScreenshotConnection);
                                        mScreenshotConnection = null;
                                        mHandler.removeCallbacks(mScreenshotTimeout);
                                    }
                                }
                            }
                        };
                        // 截屏完成后需要回调告知,由h来处理
                        msg.replyTo = new Messenger(h);
                        // 是否显示状态栏
                        msg.arg1 = 0;
                        // 是否显示导航栏
                        msg.arg2 = 1;
                        try {
                            messenger.send(msg);
                        } catch (RemoteException e) {
                        }
                    }
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {
                }
            };
            if (mContext.bindService(intent, conn, Context.BIND_AUTO_CREATE)) {
                mScreenshotConnection = conn;
                mHandler.postDelayed(mScreenshotTimeout, 10000);
            }
        }
    }
    .......
}

从上述代码中可以知道,TakeScreenshotService绑定成功后便开始往进行截屏操作,当截屏操作成功后,便会unbind这个service。

再回到TakeScreenshotService来,截屏是由GlobalScreenshot.takeScreenshot()来完成的,

    /**
     * Takes a screenshot of the current display and shows an animation.
     */
    void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
        // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
        // only in the natural orientation of the device :!)
        mDisplay.getRealMetrics(mDisplayMetrics);
        // 屏幕的高度和宽度
        float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
        // 当前屏幕所处的角度
        float degrees = getDegreesForRotation(mDisplay.getRotation());
        boolean requiresRotation = (degrees > 0);
        if (requiresRotation) {
            // Get the dimensions of the device in its native orientation
            mDisplayMatrix.reset();
            mDisplayMatrix.preRotate(-degrees);
            mDisplayMatrix.mapPoints(dims);
            dims[0] = Math.abs(dims[0]);
            dims[1] = Math.abs(dims[1]);
        }
        // Take the screenshot 进行截屏
        mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
        if (mScreenBitmap == null) {
            // 截取的图片为null,截屏失败
            notifyScreenshotError(mContext, mNotificationManager);
            // 回调告知截屏结束
            finisher.run();
            return;
        }

        if (requiresRotation) {
            // Rotate the screenshot to the current orientation
            Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
                    mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(ss);
            c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
            c.rotate(degrees);
            c.translate(-dims[0] / 2, -dims[1] / 2);
            c.drawBitmap(mScreenBitmap, 0, 0, null);
            c.setBitmap(null);
            // Recycle the previous bitmap
            mScreenBitmap.recycle();
            mScreenBitmap = ss;
        }

        // Optimizations
        mScreenBitmap.setHasAlpha(false);
        mScreenBitmap.prepareToDraw();

        // 展示动画,就是截屏后在页面上有个动画展示效果
        startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
                statusBarVisible, navBarVisible);
    }

就下来就是要去重点了解下面的代码到底干了啥

SurfaceControl.screenshot((int) dims[0], (int) dims[1])
 public static Bitmap screenshot(int width, int height) {
        // TODO: should take the display as a parameter
        IBinder displayToken = SurfaceControl.getBuiltInDisplay(
                SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN);
        return nativeScreenshot(displayToken, width, height, 0, 0, true);
    }

终于发现截屏操作竟然是在natvie层实现的,native返回了一个bitmap对象。下面移步native。nativeScreenshot方法的实现在下面的源码文件中:
frameworks\base\core\jni\android_view_SurfaceControl.cpp

static jobject nativeScreenshotBitmap(JNIEnv* env, jclass clazz, jobject displayTokenObj,
        jint width, jint height, jint minLayer, jint maxLayer, bool allLayers) {
    sp<IBinder> displayToken = ibinderForJavaObject(env, displayTokenObj);
    if (displayToken == NULL) {
        return NULL;
    }

    // 持有图像的数据
    ScreenshotPixelRef* pixels = new ScreenshotPixelRef(NULL);
    if (pixels->update(displayToken, width, height,
            minLayer, maxLayer, allLayers) != NO_ERROR) {
        delete pixels;
        return NULL;
    }

    uint32_t w = pixels->getWidth();
    uint32_t h = pixels->getHeight();
    uint32_t s = pixels->getStride();
    uint32_t f = pixels->getFormat();
    ssize_t bpr = s * android::bytesPerPixel(f);

    SkBitmap* bitmap = new SkBitmap();
    bitmap->setConfig(convertPixelFormat(f), w, h, bpr);
    if (f == PIXEL_FORMAT_RGBX_8888) {
        bitmap->setIsOpaque(true);
    }

    if (w > 0 && h > 0) {
        bitmap->setPixelRef(pixels)->unref();
        bitmap->lockPixels();
    } else {
        // be safe with an empty bitmap.
        delete pixels;
        bitmap->setPixels(NULL);
    }
    // 创建bitmap对象
    return GraphicsJNI::createBitmap(env, bitmap,
            GraphicsJNI::kBitmapCreateFlag_Premultiplied, NULL);
}
作者:andywuchuanlong 发表于2016/10/11 8:55:50 原文链接
阅读:219 评论:0 查看评论

阅读郭林《第一行代码》的笔记——第12章 Android特色开发,使用传感器

$
0
0

1、传感器简介

手机中内置的传感器是一种微型的物理设备,它能够探测、感受到外界的信号,并按一定规律转换成我们所需要的信息。Android手机通常都会支持多种类型的传感器,如光照传感右器、加速度传感器、地磁传感器、压力传感器、温度传感器等。
当然,Android系统只是负责将这些传感器所输出的信息传递给我们,至于具体如何去利用这些信息就要充分发挥开发者的想象力了。目前市场上很多的程序都有使用到传感器的功能,比如最常见的赛车游戏,玩家可以通过旋转设备来控制赛车的前进方向,就像是在操作方向盘一样。除此之外,微信的摇一摇功能,手机指南针等软件也都是借助传感器来完成的。

2、光照传感器

光照传感器在Android中的应用还是比较常见的,比如系统就有个自动调整屏幕亮度的功能。它会检测手机周围环境的光照强度,然后对手机屏幕的亮度进行相应地调整,以此保证不管是在强光还是弱光下,手机屏幕都能够看得清。
例子:
布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/txt_light_lever"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="1"
        android:textSize="20sp" />

</RelativeLayout>

代码:

package com.example.test.sensor;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;

import com.example.test.R;

/**
* Created by Administrator on 2016/6/4.
*/
public class LightSensorActivity extends Activity {
    private TextView txtLevel;
    //SensorManager是系统所有传感器的管理器,有了它的实例之后就可以调用getDefaultSensor()方法来得到任意的传感器类型了
    private SensorManager sensorManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sensor_light);
        txtLevel = (TextView) findViewById(R.id.txt_light_lever);
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        //这里使用Sensor.TYPE_LIGHT常量来指定传感器类型,此时的Sensor实例就代表着一个光照传感器
        Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        /**
        *调用SensorManager的registerListener()方法来注册SensorEventListener才能使其生效,registerListener()方法接收三个参数
        * 第一个参数就是SensorEventListener的实例,
        * 第二个参数是Sensor的实例,
        * 第三个参数是用于表示传感器输出信息的更新速率,共有SENSOR_DELAY_UI、SENSOR_DELAY_NORMAL、
        *        SENSOR_DELAY_GAME和SENSOR_DELAY_FASTEST这四种值可选,它们的更新速率是依次递增的。
        */
        sensorManager.registerListener(listener, sensor, sensorManager.SENSOR_DELAY_NORMAL);
    }

    /**
    * 调用unregisterListener()方法来释放使用的资源
    */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != sensorManager) {
            sensorManager.unregisterListener(listener);
        }
    }

    //SensorEventListener是对传感器输出的信号进行监听,SensorEventListener是一个接口,其中定义了onSensorChanged()和onAccuracyChanged()这两个方法
    private SensorEventListener listener = new SensorEventListener() {
        /**
        * 当传感器的精度发生变化时就会调用onAccuracyChanged()方法
        * @param event SensorEvent参数里又包含了一个values数组,所有传感器输出的信息都是存放在这里的。
        */
        @Override
        public void onSensorChanged(SensorEvent event) {
            //values数组中第一个下标的值就是当前的光照强度
            float value = event.values[0];
            txtLevel.setText("Current light level is " + value + " lx.");
        }

        /**
        * 当传感器监测到的数值发生变化时就会调用onSensorChanged()方法
        * @param sensor 传感器
        * @param accuracy 变化后的值
        */
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {

        }
    };

}

3、加速度传感器

Android中的加速度传感器则是提供了一种机制,使得我们能够在应用程序中获取到手机当前的加速度信息,合理利用这些信息就可以开发出一些比较好玩的功能。
接下来我们尝试利用加速度传感器来模仿一下微信的摇一摇功能。其实主体逻辑也非常简单,只需要检测手机在X轴、Y轴和Z轴上的加速度,当达到了预定值的时候就可以认为用户摇动了手机,从而触发摇一摇的逻辑。那么现在问题在于,这个预定值应该设定为多少呢?由于重力加速度的存在,即使手机在静止的情况下,某一个轴上的加速度也有可能达到9.8m/s2,因此这个预定值必定是要大于9.8m/s2的,这里我们就设定为15m/s2吧。
例子:
布局:什么都没有,

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

</LinearLayout>

代码:

package com.example.test.sensor;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.Toast;

import com.example.test.R;

/**
* Created by Administrator on 2016/6/4.
*/
public class AccelerometerSensorActivity extends Activity {
    private SensorManager sensorManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sensor_accelerometer);
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        sensorManager.registerListener(listener, sensor, sensorManager.SENSOR_DELAY_NORMAL);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != sensorManager) {
            sensorManager.unregisterListener(listener);
        }
    }

    private SensorEventListener listener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            //加速值可能为负值,所以要取他们的绝对值
            float xValue = Math.abs(event.values[0]);
            float yValue = Math.abs(event.values[1]);
            float zValue = Math.abs(event.values[2]);
            if (xValue > 15 || yValue > 15 || zValue > 15) {
                //认为用户摇动了手机,触发摇一摇逻辑
                Toast.makeText(AccelerometerSensorActivity.this, "摇一摇", Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {

        }
    };

}

4、方向传感器

要说Android中另外一个比较常用的传感器应该就是方向传感器了。方向传感器的使用场景要比其他的传感器更为广泛,它能够准确地判断出手机在各个方向的旋转角度,利用这些角度就可以编写出像指南针、地平仪等有用的工具。另外,在本章开始时介绍的通过旋转设备来控制方向的赛车游戏,也是使用方向传感器来完成的。Android获取手机旋转的方向和角度是通过加速度传感器和地磁传感器共同计算得出的,这也是Android目前推荐使用的方式。
例子:制作简易指南针
布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/img_compass"
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:layout_centerInParent="true"
        android:src="@drawable/compass" />

    <ImageView
        android:layout_width="60dp"
        android:layout_height="110dp"
        android:layout_centerInParent="true"
        android:src="@drawable/arrow" />

</RelativeLayout>

主界面:

package com.example.compasstest;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private SensorManager sensorManager;
    //指南针的背景图
    private ImageView imgCompass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imgCompass = (ImageView) findViewById(R.id.img_compass);

        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        //获取加速传感器,并为它注册监听器
        Sensor accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        sensorManager.registerListener(listener, accelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
        //获取地磁传感器,并为它注册监听器
        Sensor magneticSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        sensorManager.registerListener(listener, magneticSensor, SensorManager.SENSOR_DELAY_GAME);
        //注:SensorManager.SENSOR_DELAY_GAME为传感器输出信息的更新速度
    }

    private SensorEventListener listener = new SensorEventListener() {

        float[] accelerometerValues = new float[3];
        float[] magneticValues = new float[3];
        private float lastRotateDegree;

        @Override
        public void onSensorChanged(SensorEvent event) {
            //判断当前是加速传感器还是地磁传感器
            if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
                //注意赋值的时候要调用close方法
                accelerometerValues = event.values.clone();
            } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
                magneticValues = event.values.clone();
            }
            float[] R = new float[9];
            float[] values = new float[3];
            SensorManager.getRotationMatrix(R, null, accelerometerValues, magneticValues);
            SensorManager.getOrientation(R, values);
            //values[0]表示是手机围绕Z轴旋转的弧度,Math.toDegrees()方法是将弧度转化成角度
//            Log.i(TAG, "values[0] is " + Math.toDegrees(values[0]));
            //values[0]的取值范围是-180度到180度,其中±180度表示正南方向,0度表示正北方向,90度表示正西方向,90度表示正东方向。
            //将计算出的旋转角度取反,用于旋转指南针背景图
            float rotateDegree = -(float) Math.toDegrees(values[0]);
            if (Math.abs(rotateDegree - lastRotateDegree) > 1) {
                //创建一个动画的实例,参数:旋转的起始角度、旋转的终止角度、后面四个参数用于指定旋转的中心店
                RotateAnimation animation = new RotateAnimation(
                        lastRotateDegree, rotateDegree,
                        Animation.RELATIVE_TO_SELF, 0.5f,
                        Animation.RELATIVE_TO_SELF, 0.5f);
                animation.setFillAfter(true);
                //执行旋转动画
                imgCompass.startAnimation(animation);
                lastRotateDegree = rotateDegree;
            }
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null!=sensorManager){
            sensorManager.unregisterListener(listener);
        }
    }

}

主题设置:
在AndroidManifest文件里面种application里面的属性theme设置值为@style/AppTheme,而AppTheme里面设置

   <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowActionBar">false</item>
        <!--<item name="windowActionBar">false</item>-->
    </style>

效果图:
指南针
当然了,Android中支持的传感器远远不只这些,还有压力传感器、温度传感器、陀螺仪传感器等,不过由于这些传感器都不太常用,而且不少Android手机中都没有嵌入这些传感器。

作者:u010102829 发表于2016/10/11 9:34:39 原文链接
阅读:29 评论:0 查看评论

【android极光推送】—从客户端到后台,一文通吃

$
0
0

前记

最近一段时间,因为公司需求,需要转向Java Web方向发展,android得放下一段时间(不过还是会利用空余时间坚持写文章~)。

推送功能是app很常用的一个功能,目前能够实现推送的第三方平台也有不少,比如友盟极光信鸽等等,总之只要百度一下android推送关键字,就能看到很多的厂家。

这篇博文选择的是极光推送(老板选择的平台…),本文将从android客户端服务端实现推送,让大家对推送的全流程有一个完整的了解。

推送原理浅析

我一直认为,无论做那种开发,一定要思路先行,对要实现的功能有一个大致的理解,这样才能以不变应万变。

所以这部分,我个人认为是相比下面技术细节更为重要的内容,只要你理解的推送的大致的工作流程,那么无论你再重新使用哪个推送平台,都能得心应手。

平台说明

目前提供送服务有非常多平台,而其产品也能囊括消息推送(还有实时聊天的sdk)、短信推送等服务,其平台种类之多……各位小伙伴百度一下就知道了。其实平台的选择在前期并不是很重要(反正XX条推送内都免费),对于初次接触推送的开发者而言,弄清楚推送的工作流程,才是不变的王道。

本篇博文的推送平台采用极光推送

概念解释

在正式介绍推送的实现方式之前,先来让我理解一些基本的概念,这样有助于我们后面的理解

  • 推送服务方
    提供推送接口及集成SDK,本文中的推送服务方就是极光推送

  • 项目服务器
    每个正式的项目,肯定会有一个自己的后台,用于为APP提供接口以及保存APP产生的数据到服务器的数据库中。这里的项目服务器就是指安装并开启了wampServer的本机。

  • 推送请求
    通常情况下,我们会把跟推送相关的内容进行封装(Json的方式),里面会包含推送的内容(alert)、标题(title)、平台(platform)等,详细的选项可以参考推送服务方官网的文档,当然,推送请求的内容会因推送平台的不同而有所差异,这里以采用的推送服务方的官方文档为主。

  • 客户端
    即发出推送请求或者接受推送消息的一方,

推送的三种实现方式

客户端直接向推送服务方发送Http请求

最为简单粗暴的一种方式,一般推送服务方都会给出一个调用推送服务的API,以极光推送为例,其post的调用地址为:

POST https://api.jpush.cn/v3/pus

那么在客户端,直接使用Http,封装推送请求所需要的参数,并向这个地址发送请求

示意图:
这里写图片描述

从上图可以看到,请求直接发送给推送方,相关所有参数都需要在客户端操作

优点:直接使用推送方的服务器,所以无需配置,调用方便快捷
缺点:客户端封装大量参数,尤其在android和ios开发同时存在时,微小的变动可能意味着两边大量代码的修改,不利于后期维护

项目服务器通过Http转发推送请求至推送服务方

(1)手机客户端封装关键参数(例如推送的type)
(2)项目服务器接受请求,并再次封装一些参数,例如推送消息的titile和alert等
(3)项目服务器作为客户端,向推送方服务器发送Http请求;

首先应该明确的一点是,服务端和客户端是一个相对的概念,可以简单地提交请求的是客户端,处理请求的是服务端。

此时客户端的编程就会很轻松了(就是写一条网络请求就可以了)。麻烦起来的是服务端,因为这二种方法的本质是:把本该由客户端发起的相对复杂的Http请求交给了服务端完成一部分

那么这种方法和第一种方法的区别是什么呢?

第一种方法,发送推送请求的客户端是移动端设备,在第二种方法中,发送推送请求的客户端,是项目服务器,两种方法接受推送请求的服务端,都是极光推送。客户端不直接与推送服务器打交道。

示意图:(动图太难做了(:з」∠)……还是简单的示意图吧)
这里写图片描述

优点:项目服务器对客户端请求进行了一定封装,当后期需求变动时,利于维护。
缺点:独立开发的话在配置服务器时有点麻烦……其他的看不出什么明显缺点(第二种方式可以和第三中方式做下对比)

项目服务端使用SDK进行功能集成

第三种实现方式和第二种在大致的工作流程上没有什么太大的区别,主要的不同在于,项目服务器采用了第三方的SDK,在本地集成了之后,只需非常简单的调用一行代码,那就能完成推送的功能(SDK的底层肯定还是网络请求,只不过做了相对完善的封装)。

这里写图片描述

可以看到极光推送为各种脚本语言编写的服务端都匹配了相应的sdk,还是比较方便的。

第三种方法的工作流程和第二种差不多,这里就不在赘述了。

关于推送的种类概述

在实际使用的过程中,推送的对象大致分为如下几种:

(1)无对象差别,全平台推送
(2)无对象差别,特定平台推送
(3)特定对象,全平台推送
(3)特定群组,全平台推送
(4)特定群组,特定平台推送
(5)特定对象,特定台对送

反正就是 对象、群组、平台排列组合,根据项目需求进行设定。

一般比较常见的是特定对象推送,比如QQ消息这种,就是针对某一用户进行推送。

实现这种类型的推送,需要在客户端启动的时候为用户设置一个Alias(相当于唯一标识符),或者tag(表示用户所属群组),来进行特定推送。

关于推送的基本内容就介绍到这里,更多详细的内容,可以参考官方文档~

android客户端初步实现

集成SDK说明

讲道理,无论是那个第三方平台,都会提供详细的SDK开发文档给开发者,所以说只要开发者有点耐心,一点点按照官方文档上的去实现,遇到不懂的地方度娘一下,基本上就可以实现SDK的集成。

这里我就不完全copy官网的文档,而是在官方文档的基础上,用我自己的语言去描述一下sdk的集成过程,怕我描述有误的小伙伴可以去参考更加权威的官方文档

当然随着官网集成包的更新,可能我现在在博文里写的方法会过时,所以一切还是以官网的内容为准。

好了,废话到此位置。

集成步骤

1、下载官方提供的SDK集成包

传送门:客户端 SDK 下载
然后解压缩,看到如下目录:
这里写图片描述

同时我们在自己项目的src/main目录下面新建一个文件夹jniLibs,将SDK包libs下面的CPU类型放入我们自己创建的jniLibs里
这里写图片描述

2、手动导入SDK

官方提供了2中导入的方法,一种是jCenter自动导入,一种是手动导入,这里我们采用后者(前者的方法详见官方文档)

(1)将lib目录下的jpush-android-2.1.9.jar包导入到项目的lib目标下
这里写图片描述

(2)将jar包添加为主工程的依赖。右键jar包,点击add as Library,选择app,确认。
当操作完毕后,我们可以通过FIle->project structure->app->Dependcies 查看主工程添加依赖。

这里写图片描述
可以看到已经添加到了主工程的依赖当中了

3、在极光的官网创建一个应用

我们要使用极光的推送服务,就必须在其官网创建一个开发者账号(任何第三方推送平台实际上都需要这么做)

这里写图片描述

创建的过程比较简单,这里就不详细描述了,初学者需要稍微注意一下项目完整的包名

这里写图片描述

4、编写一个MyApplication类,初始化SDK

初始化SDK,没什么好说的

MyApplication

package com.example.dell.imooc_jpushdemo;

import cn.jpush.android.api.JPushInterface;

/**
 * Created by dell on 2016/9/23.
 */
public class MyApplication extends android.app.Application {
    @Override
    public void onCreate() {
        super.onCreate();
        JPushInterface.setDebugMode(true);
        JPushInterface.init(this);
    }
}

5、配置 AndroidManifest.xml

这里是我项目配置的 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dell.imooc_jpushdemo">

    <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23" />

    <!-- Required -->
    <permission
        android:name="com.example.dell.imooc_jpushdemo.permission.JPUSH_MESSAGE"
        android:protectionLevel="signature" />

    <!-- Required -->
    <uses-permission android:name="com.example.dell.imooc_jpushdemo.permission.JPUSH_MESSAGE" />
    <uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:name=".MyApplication"
        android:theme="@style/AppTheme">

        <!-- Required. For publish channel feature -->
        <!-- JPUSH_CHANNEL 是为了方便开发者统计APK分发渠道。-->
        <!-- 例如: -->
        <!-- 发到 Google Play 的APK可以设置为 google-play; -->
        <!-- 发到其他市场的 APK 可以设置为 xxx-market。 -->
        <!-- 目前这个渠道统计功能的报表还未开放。-->
        <meta-data android:name="JPUSH_CHANNEL" android:value="developer-default"/>
        <!-- Required. AppKey copied from Portal -->
        <meta-data android:name="JPUSH_APPKEY" android:value="52f7fd72d96df72e2a811d7c"/>


        <!-- Required -->
        <receiver
            android:name="cn.jpush.android.service.PushReceiver"
            android:enabled="true" >
            <intent-filter android:priority="1000">
                <action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" />
                <category android:name="com.example.dell.imooc_jpushdemo"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.USER_PRESENT" />
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            </intent-filter>
            <!-- Optional -->
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_ADDED" />
                <action android:name="android.intent.action.PACKAGE_REMOVED" />
                <data android:scheme="package" />
            </intent-filter>
        </receiver>
        <!-- Required SDK核心功能-->
        <activity
            android:name="cn.jpush.android.ui.PushActivity"
            android:configChanges="orientation|keyboardHidden"
            android:exported="false" >
            <intent-filter>
                <action android:name="cn.jpush.android.ui.PushActivity" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="com.example.dell.imooc_jpushdemo" />
            </intent-filter>
        </activity>
        <!-- Required SDK核心功能-->
        <service
            android:name="cn.jpush.android.service.DownloadService"
            android:enabled="true"
            android:exported="false" >
        </service>
        <!-- Required SDK核心功能-->
        <receiver android:name="cn.jpush.android.service.AlarmReceiver" />


        <!-- Required SDK 核心功能-->
        <!-- option since 2.0.5 可配置PushService,DaemonService,PushReceiver,AlarmReceiver的android:process参数 将JPush相关组件设置为一个独立进程 -->
        <!-- 如:android:process=":remote" -->
        <service
            android:name="cn.jpush.android.service.PushService"
            android:enabled="true"
            android:exported="false" >
            <intent-filter>
                <action android:name="cn.jpush.android.intent.REGISTER" />
                <action android:name="cn.jpush.android.intent.REPORT" />
                <action android:name="cn.jpush.android.intent.PushService" />
                <action android:name="cn.jpush.android.intent.PUSH_TIME" />
            </intent-filter>
        </service>



        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

至此androidSDK的配置就完成了,现在我们可以去极光的去推送一下消息,如果应用可以收到推送,这说明配置正确。

wampServer服务端配置

服务端这里采用wampServer的方式,当然是处于简单方便的考虑,还可以使用其他的脚本语言来实现,而极光推送的官网也提供了相应的服务端SDK供大家使用

这里的继承方式采用官方提供的sdk(也就是本文介绍的第三种方法),当然,也可以使用PHP向推送方直接发送HTTP请求。

配置推送SDK

极光官方建议使用composer进行SDK安装,而且给了非常简单的介绍
这里写图片描述

接下来介绍一下composer的安装。

通过composer配置

关于什么是composer,我个人把它理解为android里面的gradle,在android里面,我们可以在gradle中添加一句 “compile XXX”就能轻松引入第三方库,而composer的功能也是如此,composer是 PHP 用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。

这里简单介绍一下composer的下载,不放心的小伙伴可以去Composer中文网相关查看教程。

(1)从官网下载composer

传送门:Windows下载

一路next,安装完毕后在cmd里面输入composer

这里写图片描述

即说明下载成功。

(2)关于composer.phar

在查阅资料的时候,我看到了composer.phar文件,后来研究了一下,得出了一些结论。

composer.phar 可以理解为composer的一个命令集合,不需要无需下载配置composer环境,只需要在PHP环境下,即可被使用,基本的使用方法是

php composer.phar XXXX

XXX表示composer的命令

而我们在这第一步下载的composer.exe,它实际做的一件事情是下载composer各种命令,并把composer添加到环境变量中(这也是为什么我们安装完毕后,在dos界面输入composer能够有反应的原因),其用法是:

composer XXX

发现没,这里的composer无需使用php,就能够执行composer的命令

综上述所述,如果你不想安装composer.exe,下载一个composer.phar文件,然后在PHP环境下运行就可以了,二者没有太大的差别。

不过有一点需要注意的是,命令的运行目录,一定要是当前的项目目录(存放有composer.json)。

(3)编写composer.json文件

此文件的作用主要用来声明包之间的相互关系和其他的一些元素标签。
接下来,我们来到项目目录(我的是wamp/www/push)
在此目录下新建一个叫 composer.json的文件

这里写图片描述

在此文件里面输入后保存,退出;

{
    "require": {
        "jpush/jpush": "v3.5.*"
    }
}

(4)下载第三方库

接下来,我们需要在cmd当中不断的cd目录,一直进入到我们当前的push目录,这里教大家一个简单的方法。如果你装了git,那么直接右键push目录,点击git brash here 就能很方便地打开dos界面。

键入下载的命令:

composer install

或者

php composer.phar install

二者的区别请见第二点

这里写图片描述

在解释上面问题之前,需要讲讲composer.lock这个文件

在安装完所有需要的包之后,composer会生成一张标准的包版本的文件在composer.lock文件中。这将锁定所有包的版本。

使用composer.lock(当然是和composer.json一起)来控制你的项目的版本

这一点非常的重要,我们使用install命令来处理的时候,它首先会判断composer.lock文件是否存在,如果存在,将会下载相对应的版本(不会在于composer.json里面的配置),这意味着任何下载项目的人都将会得到一样的版本。

如果不存在composer.lock,composer将会通过composer.json来读取需要的包和相对的版本,然后创建composer.lock文件

这样子就可以在你的包有新的版本之后,你不会自动更新了,升级到新的版本,使用update命令即可,这样子就能获取最新版本的包并且也更新了你的composer.lock文件。

所以,第一次的话,使用install命令,之后的使用updata命令就好了。

全部下载完成后,我们的当前的项目目录应该是这个样子的。

这里写图片描述

编写推送接口

终于到了编写接口的部分了

为了方便的加载包文件,Composer自动生成了一个文件 vendor/autoload.PHP,我们可以方便只有的使用它在任何你需要使用的地方。

新建一个test.php文件 在里面实现我们的推送逻辑。

<?php
//composer下载下来的第三方SDK都放在vendor文件夹中
//注意路径
require 'vendor/jpush/jpush/autoload.php';

//接受post来的参数
$params=$_POST;

//创建应用的AppKey和master_secret,可以在极光应用后台查看
$app_key="52f7fd72d96df72e2a811d7c";
$master_secret="847d609885ec219f313b0c12";

/*
 * 官方代码
 * */
$client = new \JPush\Client($app_key, $master_secret);

$pusher = $client->push();
//设置平台
$pusher->setPlatform('all');

$pusher->addAllAudience();

//唯一标识符,暂不使用
//$pusher->addAlias($params['alias']);
// 简单地给所有平台推送相同的 alert 消息

$pusher->setNotificationAlert('Hello, JPush');
try {
    $result=$pusher->send();
    var_dump($result);
} catch (\JPush\Exceptions\JPushException $e) {
    // try something else here
    print $e;
}

官方文档里有更详细的介绍,相信大家结合上面的注释应该是可以看懂的。

这个API的作用是向所有的客户发送一条消息。

现在把wampServer运行起来,在浏览器输入这个接口。

http://localhost/push/test.php

这里写图片描述

如果调用成功,则说明接口正确~~

定向推送

到这里,从客户端到服务端的基本推送逻辑已经完成了,接下来我们做的一件事情就是实现定向的推送,即通过这个alias,将消息发送给指定的用户

android客户端改动

首先填写一个布局文件MainActivity.xml

效果如下:
这里写图片描述

比较简陋,一个editText用于设定alias,一个editText用于输入目标alias,还有一个Spinner用于选择推送的类型。

xml代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.dell.imooc_jpushdemo.MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <EditText
            android:id="@+id/et_set_alias"
            android:gravity="center"
            android:hint="设定alias"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/bt_set"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="设置"
           />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="当前alias:未设置"
            android:id="@+id/tv_alias"
            android:textSize="18sp"
            android:layout_marginTop="22dp" />


        <EditText
            android:id="@+id/et_alias"
            android:gravity="center"
            android:layout_marginTop="20dp"
            android:hint="输入目标alias"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
        />

            <Spinner
                android:id="@+id/push_type"
                android:layout_width="match_parent"
                android:layout_height="30dp"
             />

            <Button
                android:id="@+id/bt_send"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="发送"
                android:layout_marginTop="23dp" />

            <TextView
                android:id="@+id/tv_result"
                android:text="推送结果"
                android:gravity="center"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
        />
        </LinearLayout>
</RelativeLayout>

接下来,我们编写MainActivity当中的逻辑

public class MainActivity extends AppCompatActivity {
    //接口地址,根据本机的IP地址改动
    private final String url="http://192.168.0.128/push/test.php";
    //返回的结果
    private TextView tvResult;
    private Button btSend; //发送推送请求
    private Spinner mSpinner; //选择推送方式
    //推送种类
    private String pushType;
    //spinner的适配器
    private ArrayAdapter<String> adapter;
    //设置alias的按钮
    private Button btSetAlias;
    //显示用户设置的alias
    private TextView tvAlias;
    private String alias;

    //更新UI
    private Handler handler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            StringBuffer sb = (StringBuffer) msg.obj;
            tvResult.setText(sb.toString());
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mSpinner= (Spinner) findViewById(R.id.push_type);
        tvResult= (TextView) findViewById(R.id.tv_result);
        btSend= (Button) findViewById(R.id.bt_send);
        btSetAlias= (Button) findViewById(R.id.bt_set);
        tvAlias= (TextView) findViewById(R.id.tv_alias);
        //spinner内的文字
        String[] strings=getResources().getStringArray(R.array.push_type);
        List<String> list=new ArrayList<>();
        for(String s:strings){
            list.add(s);
        }

        adapter=new ArrayAdapter<String>(MainActivity.this,android.R.layout.simple_spinner_item,list);
        //第三步:为适配器设置下拉列表下拉时的菜单样式。
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        //第四步:将适配器添加到下拉列表上
        mSpinner.setAdapter(adapter);

        mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                pushType=adapter.getItem(i);
                    /* 将mySpinner 显示*/
                adapterView.setVisibility(View.VISIBLE);
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {
                adapterView.setVisibility(View.VISIBLE);
            }
        });

        //设置alias
        btSetAlias.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                alias=((EditText)findViewById(R.id.et_set_alias)).getText().toString();
                //调用SDK接口
                JPushInterface.setAlias(getBaseContext(),alias, new TagAliasCallback() {
                    @Override
                    public void gotResult(int i, String s, Set<String> set) {
                        tvAlias.setText("当前alias:"+alias);
                        Toast.makeText(MainActivity.this, "设置成功", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });

        btSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                sendRequest();
            }
        });
    }

    private void sendRequest() {
        new Thread(new Runnable() {
            @Override
            public void run() {

                String alias=((EditText)findViewById(R.id.et_alias)).getText().toString();
                try {
                    // 传递的数据
                    String data ="&alias="+URLEncoder.encode(alias,"UTF-8")
                            +"&push_type="+URLEncoder.encode(pushType,"UTF-8");
                    URL httpUrl = new URL(url);
                    HttpURLConnection urlConnection = (HttpURLConnection) httpUrl.openConnection();
                    urlConnection.setRequestMethod("POST");

                    // 设置请求的超时时间
                    urlConnection.setReadTimeout(5000);
                    urlConnection.setConnectTimeout(5000);

                    //调用conn.setDoOutput()方法以显式开启请求体
                    urlConnection.setDoOutput(true); // 发送POST请求必须设置允许输出
                    urlConnection.setDoInput(true); // 发送POST请求必须设置允许输入

                    //setDoInput的默认值就是true
                    OutputStream ost = urlConnection.getOutputStream();

                    PrintWriter pw = new PrintWriter(ost);
                    pw.print(data);
                    pw.flush();
                    pw.close();

                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                    StringBuffer sb = new StringBuffer();
                    String s;
                    while ((s = bufferedReader.readLine()) != null) {
                        sb.append(s);
                        Log.d("111", "run: "+sb.toString());
                    }
                    Message msg = new Message();
                    msg.obj = sb;
                    handler.sendMessage(msg);
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

R.array.push_type里面的内容:(在res/value/arrays.xml中配置,如果没有这个xml文件,手动创建)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="push_type">
        <item>push Type</item>
        <item>new_task</item>
        <item>send_task</item>
        <item>get_task</item>
        <item>finish_task</item>
    </string-array>
</resources>

注释都比较清楚,网络操作使用的是调用android原生的API,当然也可以使用一些网络框架来完成。

最后,我们对服务端的代码进行改动

服务端改动

服务端test.php

<?php
//composer下载下来的第三方SDK都放在vendor文件夹中
//注意路径
require 'vendor/jpush/jpush/autoload.php';

//接受post来的参数
$params=$_POST;

//创建应用的AppKey和master_secret,可以在极光应用后台查看
$app_key="52f7fd72d96df72e2a811d7c";
$master_secret="847d609885ec219f313b0c12";



$title='';
$alert='';
//接受客户端参数
@$alias=$_POST['alias'];
@$push_type=$_POST['push_type'];

if(empty($alias)){
   echo 'alias null';
   return;
}
switch ($push_type){
    case "new_task":
        $title="新订单提醒";
        $alert="您有新的订单";
        break;
    case "send_task":
        $title='订单指派提醒';
        $alert='您的订单已被指派';
        break;
    case "get_task":
        $title='订单受理提醒';
        $alert='您有订单已被受理';
        break;
    case "finish_task":
        $title='订单完成提醒';
        $alert='您的订单已完成';
        break;
}

/*
 * 官方代码
 * */
$client = new \JPush\Client($app_key, $master_secret);

$pusher = $client->push();
//设置平台
$pusher->setPlatform('all');

//唯一标识符
$pusher->addAlias($alias);

echo $alert." ".$title;

$pusher->setNotificationAlert($title, $alert);
try {
    $result=$pusher->send();
    var_dump($result);
} catch (\JPush\Exceptions\JPushException $e) {
    // try something else here
    print $e;
}

服务端这里新增了对post参数的接收,当然……这里的参数验证逻辑写的还不够严谨,在实际项目中肯定还要有更多的验证及错误信息反馈。

PHP端代码相对比较简单……当然,写的还不够优雅,不过已经可以实现基本的功能了~

至此,代码部分就全部完成了,现在让我们来测试一下效果!

效果测试

首先我开启了2台虚拟机,并且提前设置好了他们的alias

夜神模拟器:
这里写图片描述

android模拟器:
这里写图片描述

夜神模拟机的alias设为001,推送的type设为new_task
as 的模拟机alias 设为002,推送的type设为send_task

接下来,我们点击推送

这里写图片描述
可以看到,两台机器都成功收到了相应的推送!

结语(终于完成了……)

国庆期间花了不少时间折腾这个……现在才整理出来……(不过好在整理出来了)

这个demo从客户端到后台,相对比较全面地介绍了推送的相关内容,当然,这些内容在官网上都能找到相应的资料,而且这篇文章的代码还不够优雅(尤其是PHP部分),实现的功能还比较基础,所以要完成更加复杂功能的小伙伴,还需要去官网深入学习文档,以满足实际的需求。

好了~最后感谢坚持看到了这里的小伙伴(≧▽≦)/

Git 项目地址:
https://github.com/huyifan/Imooc_JpushDemo

作者:w8897282 发表于2016/10/11 10:13:18 原文链接
阅读:162 评论:0 查看评论

深入Java源码解析容器类List、Set、Map

$
0
0

本篇文章带你从Java源码深入解析关于Java容器的概念。

原文简书地址:http://www.jianshu.com/p/047e33fdefd2

参考文献:

  1. Java容器相关知识全面总结

  2. Java官方API文档

1 常用容器继承关系图

先上一张网上的继承关系图

集合继承关系图

个人觉得有些地方不是很准确,比如Iterator不是容器,只是一个操作遍历集合的方法接口,所以不应该放在里面。并且Map不应该继承自Collection。所以自己整理了一个常用继承关系图如下

New集合继承关系图

如上图所示,接下去会自顶向下解释重要的接口和实现类。

2 Collection和Map

在Java容器中一共定义了2种集合, 顶层接口分别是Collection和Map。但是这2个接口都不能直接被实现使用,分别代表两种不同类型的容器。

简单来看,Collection代表的是单个元素对象的序列,(可以有序/无序,可重复/不可重复 等,具体依据具体的子接口Set,List,Queue等);Map代表的是“键值对”对象的集合(同样可以有序/无序 等依据具体实现)

2.1 Collection

根据Java官方文档对Collection的解释

The root interface in the collection hierarchy. A collection represents a group of objects, known as its elements. Some collections allow duplicate elements and others do not. Some are ordered and others unordered. The JDK does not provide any direct implementations of this interface: it provides implementations of more specific subinterfaces like Set and List. This interface is typically used to pass collections around and manipulate them where maximum generality is desired.

大概意思就是

是容器继承关系中的顶层接口。是一组对象元素组。有些容器允许重复元素有的不允许,有些有序有些无序。 JDK不直接提供对于这个接口的实现,但是提供继承与该接口的子接口比如 List Set。这个接口的设计目的是希望能最大程度抽象出元素的操作。

接口定义:

public interface Collection<E> extends Iterable<E> {

    ...

}

泛型即该Collection中元素对象的类型,继承的Iterable是定义的一个遍历操作接口,采用hasNext next的方式进行遍历。具体实现还是放在具体类中去实现。

我们可以看下定义的几个重要的接口方法

add(E e) 
 Ensures that this collection contains the specified element

clear()
 Removes all of the elements from this collection (optional operation).

contains(Object o)
 Returns true if this collection contains the specified element.

isEmpty()
 Returns true if this collection contains no elements.

iterator()
 Returns an iterator over the elements in this collection.

remove(Object o)
 Removes a single instance of the specified element from this collection, if it is present (optional operation).

retainAll(Collection<?> c)
 Retains only the elements in this collection that are contained in the specified collection (optional operation).(**ps:这个平时倒是没注意,感觉挺好用的接口,保留指定的集合**)

size()
 Returns the number of elements in this collection.

toArray()
 Returns an array containing all of the elements in this collection.

toArray(T[] a)
 Returns an array containing all of the elements in this collection; the runtime type of the returned array is that of the specified array.(**ps:这个接口也可以mark下**)

 ...

上面定义的接口就代表了Collection这一类容器最基本的操作,包括了插入,移除,查询等,会发现都是对单个元素的操作,Collection这类集合即元素对象的存储。其中有2个接口平时没用过但是觉得很有用

  1. retainAll(Collection

2.2 Map

Java官方文档对Map的解释

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

This interface takes the place of the Dictionary class, which was a totally abstract class rather than an interface.

The Map interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings. The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. Some map implementations, like the TreeMap class, make specific guarantees as to their order; others, like the HashMap class, do not.

大概意思就是

一个保存键值映射的对象。 映射Map中不能包含重复的key,每一个key最多对应一个value。

这个接口替代了原来的一个抽象类Dictionary。

Map集合提供3种遍历访问方法,1.获得所有key的集合然后通过key访问value。2.获得value的集合。3.获得key-value键值对的集合(key-value键值对其实是一个对象,里面分别有key和value)。 Map的访问顺序取决于Map的遍历访问方法的遍历顺序。 有的Map,比如TreeMap可以保证访问顺序,但是有的比如HashMap,无法保证访问顺序。

接口定义如下:

public interface Map<K,V> {

    ...

    interface Entry<K,V> {
        K getKey();
        V getValue();
        ...
    } 
}

泛型

3 List、Set和Queue

在Collection这个集成链中,我们介绍List、Set和Queue。其中会重点介绍List和Set以及几个常用实现class。Queue平时实在没用过。

先简单概述下List和Set。他们2个是继承Collection的子接口,就是说他们也都是负责存储单个元素的容器。但是最大的区别如下

  1. List是存储的元素容器是有个有序的可以索引到元素的容器,并且里面的元素可以重复。
  2. Set里面和List最大的区别是Set里面的元素对象不可重复。

3.1 List

Java文档中介绍

An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.

Unlike sets, lists typically allow duplicate elements. More formally, lists typically allow pairs of elements e1 and e2 such that e1.equals(e2), and they typically allow multiple null elements if they allow null elements at all. It is not inconceivable that someone might wish to implement a list that prohibits duplicates, by throwing runtime exceptions when the user attempts to insert them, but we expect this usage to be rare.

The List interface provides a special iterator, called a ListIterator, that allows element insertion and replacement, and bidirectional access in addition to the normal operations that the Iterator interface provides. A method is provided to obtain a list iterator that starts at a specified position in the list.

大概意思是

一个有序的Collection(或者叫做序列)。使用这个接口可以精确掌控元素的插入,还可以根据index获取相应位置的元素。

不像Set,list允许重复元素的插入。有人希望自己实现一个list,禁止重复元素,并且在重复元素插入的时候抛出异常,但是我们不建议这么做。

List提供了一种特殊的iterator遍历器,叫做ListIterator。这种遍历器允许遍历时插入,替换,删除,双向访问。 并且还有一个重载方法允许从一个指定位置开始遍历。

然后我们再看下List接口新增的接口,会发现add,get这些都多了index参数,说明在原来Collection的基础上,List是一个可以指定索引,有序的容器。在这注意以下添加的2个新Iteractor方法。

ListIterator<E> listIterator();

ListIterator<E> listIterator(int index);

我们再看ListIterator的代码

public interface ListIterator<E> extends Iterator<E> {
    // Query Operations

    boolean hasNext();

    E next();

    boolean hasPrevious();

    E previous();

    int previousIndex();

    void remove();

    void set(E e);

    void add(E e);
}

一个集合在遍历过程中进行插入删除操作很容易造成错误,特别是无序队列,是无法在遍历过程中进行这些操作的。但是List是一个有序集合,所以在这实现了一个ListIteractor,可以在遍历过程中进行元素操作,并且可以双向访问。

这个是之前开发中一直没有发现的,好东西。mark

以上就是List的基本概念和规则,下面我们介绍2个常用List的实现类,ArrayList和LinkedList。

3.1.1 ArrayList

就Java文档的解释,整理出以下几点特点:

  1. ArrayList是一个实现了List接口的可变数组
  2. 可以插入null
  3. 它的size, isEmpty, get, set, iterator,add这些方法的时间复杂度是O(1),如果add n个数据则时间复杂度是O(n).
  4. ArrayList不是synchronized的。

然后我们来简单看下ArrayList源码实现。这里只写部分源码分析。

所有元素都是保存在一个Object数组中,然后通过size控制长度。

transient Object[] elementData;

private int size;

这时候看下add的代码分析

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

其实在每次add的时候会判断数据长度,如果不够的话会调用Arrays.copyOf,复制一份更长的数组,并把前面的数据放进去。

我们再看下remove的代码是如何实现的。

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

其实就是直接使用System.arraycopy把需要删除index后面的都往前移一位然后再把最后一个去掉。

PS:终于发现以前学习的数据结构用到用场了。O。O

3.1.2 LinkedList

LinkedList是一个链表维护的序列容器。和ArrayList都是序列容器,一个使用数组存储,一个使用链表存储。

数组和链表2种数据结构的对比:

  1. 查找方面。数组的效率更高,可以直接索引出查找,而链表必须从头查找。
  2. 插入删除方面。特别是在中间进行插入删除,这时候链表体现出了极大的便利性,只需要在插入或者删除的地方断掉链然后插入或者移除元素,然后再将前后链重新组装,但是数组必须重新复制一份将所有数据后移或者前移。
  3. 在内存申请方面,当数组达到初始的申请长度后,需要重新申请一个更大的数组然后把数据迁移过去才行。而链表只需要动态创建即可。

如上LinkedList和ArrayList的区别也就在此。根据使用场景选择更加适合的List。

下面简单展示LinkedList的部分源码解析。

首先是链表的节点的定义,非常简单的一个双向链表。

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

然后每个LinkedList中会持有链表的头指针和尾指针

transient int size = 0;

transient Node<E> first;

transient Node<E> last;

列举最基本的插入和删除的链表操作

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

上面6个方法就是链表的核心,头尾中间插入,头尾中间删除。其他对外的调用都是围绕这几个方法进行操作的

同时LinkedList还实现了Deque接口,Deque接口是继承Queue的。所以LinkedList还支持队列的pop,push,peek操作。

总结

List实现 使用场景 数据结构
ArrayList 数组形式访问List链式集合数据,元素可重复,访问元素较快 数组
LinkedList 链表方式的List链式集合,元素可重复,元素的插入删除较快 双向链表

3.2 Set

Set的核心概念就是集合内所有元素不重复。在Set这个子接口中没有在Collection特别实现什么额外的方法,应该只是定义了一个Set概念。下面我们来看Set的几个常用的实现HashSet、LinkedHashSet、TreeSet

3.2.1 HashSet

HashSet的核心概念。Java文档中描述

This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time. This class permits the null element.

大概意思是

HashSet实现了Set接口,基于HashMap进行存储。遍历时不保证顺序,并且不保证下次遍历的顺序和之前一样。HashSet中允许null元素。

进入到HashSet源码中我们发现,所有数据存储在

private transient HashMap<E,Object> map;

private static final Object PRESENT = new Object();

意思就是HashSet的集合其实就是HashMap的key的集合,然后HashMap的val默认都是PRESENT。HashMap的定义即是key不重复的集合。使用HashMap实现,这样HashSet就不需要再实现一遍。

所以所有的add,remove等操作其实都是HashMap的add、remove操作。遍历操作其实就是HashMap的keySet的遍历,举例如下

...
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

public boolean contains(Object o) {
    return map.containsKey(o);
}

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

public void clear() {
    map.clear();
}
...

3.2.2 LinkedHashSet

LinkedHashSet的核心概念相对于HashSet来说就是一个可以保持顺序的Set集合。HashSet是无序的,LinkedHashSet会根据add,remove这些操作的顺序在遍历时返回固定的集合顺序。这个顺序不是元素的大小顺序,而是可以保证2次遍历的顺序是一样的。

类似HashSet基于HashMap的源码实现,LinkedHashSet的数据结构是基于LinkedHashMap。过多的就不说了。

3.2.3 TreeSet

TreeSet即是一组有次序的集合,如果没有指定排序规则Comparator,则会按照自然排序。(自然排序即e1.compareTo(e2) == 0作为比较)

注意:TreeSet内的元素必须实现Comparable接口。

TreeSet源码的算法即基于TreeMap,具体算法在说明TreeMap的时候进行解释。

总结

Set实现 使用场景 数据结构
HashSet 无序的、无重复的数据集合 基于HashMap
LinkedSet 维护次序的HashSet 基于LinkedHashMap
TreeSet 保持元素大小次序的集合,元素需要实现Comparable接口 基于TreeMap

4 HashMap、LinkedHashMap、TreeMap和WeakHashMap

4.1 HashMap

HashMap就是最基础最常用的一种Map,它无序,以散列表的方式进行存储。之前提到过,HashSet就是基于HashMap,只使用了HashMap的key作为单个元素存储。

HashMap的访问方式就是继承于Map的最基础的3种方式,详细见上。在这里我具体分析一下HashMap的底层数据结构的实现。

在看HashMap源码前,先理解一下他的存储方式-散列表(哈希表)。像之前提到过的用数组存储,用链表存储。哈希表是使用数组和链表的组合的方式进行存储。(具体哈希表的概念自行搜索)如下图就是HashMap采用的存储方法。

哈希散列表

hash得到数值,放到数组中,如果遇到冲突则以链表方式挂在下方。

HashMap的存储定义是

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

数组table存放元素,如果遇到冲突下挂到冲突元素的next链表上。

在这我们可以看下get核心方法和put核心方法的源码

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

上面代码中看出先根据hash值和数组长度作且运算得出下标索引。如果存在判断hash值是否完全一致,如果不完全一致则next链表向下找一致的hash值。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

上面是put的核心源码,即查找hash值所在索引是否有元素,没有的话new一个Node直接放在table中。如果已经有Node了,就遍历该Node的next,将新元素放到最后。

HashMap的遍历,是从数组遍历第一个非空的元素,然后再根据这个元素访问其next下的所有Node。因为第一个元素不是一定从数组的0开始,所以HashMap是无序遍历。

4.2 LinkedHashMap

LinkedHashMap相对于HashMap来说区别是,LinkedHashMap遍历的时候具有顺序,可以保存插入的顺序,(还可以设置最近访问的元素也放在前面,即LRU)

其实LinkedHashMap的存储还是跟HashMap一样,采用哈希表方法存储,只不过LinkedHashMap多维护了一份head,tail链表。

transient LinkedHashMap.Entry<K,V> head;

transient LinkedHashMap.Entry<K,V> tail;

即在创建新Node的时候将新Node放到最后,这样遍历的时候不再像HashMap一样,从数组开始判断第一个非空元素,而是直接从表头进行遍历。这样即满足有序遍历。

4.3 TreeMap

TreeMap平时用的不多,TreeMap会实现SortMap接口,定义一个排序规则,这样当遍历TreeMap的时候,会根据规定的排序规则返回元素。

4.4 WeakHashMap

WeakHashMap,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值,

举例:声明了两个Map对象,一个是HashMap,一个是WeakHashMap,同时向两个map中放入a、b两个对象,当HashMap remove掉a 并且将a、b都指向null时,WeakHashMap中的a将自动被回收掉。出现这个状况的原因是,对于a对象而言,当HashMap remove掉并且将a指向null后,除了WeakHashMap中还保存a外已经没有指向a的指针了,所以WeakHashMap会自动舍弃掉a,而对于b对象虽然指向了null,但HashMap中还有指向b的指针,所以
WeakHashMap将会保留。

WeakHashMap用的也不多,在这简单提及。

总结

Map实现 使用场景 数据结构
HashMap 哈希表存储键值对,key不重复,无序 哈希散列表
LinkedHashMap 是一个可以记录插入顺序和访问顺序的HashMap 存储方式是哈希散列表,但是维护了头尾指针用来记录顺序
TreeMap 具有元素排序功能 红黑树
WeakHashMap 弱键映射,映射之外无引用的键,可以被垃圾回收 哈希散列表

结尾

以上就是对于Java集合的完整分析和源码解析。其中ArrayList、HashMap使用较多,当考虑到效率时记得有Linded系列集合和WeakHashMap。Over~~

最后附上我的微信公众号和简书博客地址。

微信公众号

简书:http://www.jianshu.com/users/21716b19302d/latest_articles

作者:tsy12321 发表于2016/10/11 10:13:51 原文链接
阅读:64 评论:0 查看评论

uC/OS-II任务就绪表及任务调度

$
0
0

最近开始花时间去学习uc/OS-II,一方面是工作上用的是这个系统,另一方面就是想去了解实时操作系统与普通操作系统的区别,学到任务就绪表及任务调度这里,对实时的概念有所了解,所以写此文帮助自己梳理,也希望与读者交流。

实时含有立即、及时之意。如果操作系统能使计算机系统及时响应外部事件的请求,并能及时控制所有实时设备与实时任务协调运行,且能在一个规定的事件内完成对事件的处理,那么这种操作系统就是一个实时操作系统。

对实时系统有两个基本要求,第一,实时操作系统的计算必须产生正确的结果,称为逻辑或功能正确。第二,实时系统的计算必须在预定的事件内完成,称为时间正确。为了达到这些要求,实时操作系统应满足以下三个条件:
1)实时操作系统必须是多任务系统
2)任务的切换时间应与系统中的任务数无关
3)中断延迟的时间可预知并尽可能短

本文主要关于第二点,任务的切换时间是如何与任务数无关。

===========================分割线============================

为系统中处于就绪状态的任务分配CPU是多任务操作系统的核心工作。内核在进行任务调度时,必须知道哪个任务在运行、哪个任务是就绪的最高优先级的任务。实时任务调度的要求无论系统的运行情况如何,调度的时间是确定的,不能把时间都用在调度上。为满足这样的需要,uC/OS-II系统里定义了任务就绪表结构

任务就绪表如下所示:
这里写图片描述

这个任务就绪表登记了系统中所有处于就绪状态的任务,它是一个位图,系统中的每个任务都在这个位图中占据一个二进制位,该位值的状态就表示任务是否处于就绪状态。实际上,它是一个类型为INT8U的数组OSRdyTbl[],它以任务优先级别的高低为顺序,为每个任务安排了一个二进制位。

在上图中,该数组有8个元素,即可以表达64个任务的就绪状态。每个元素描述了8个任务的就绪状态,于是8个任务可看成一个任务组,为了便于对就绪表进行查找,uC/OS-II又定义了一个数据类型为INT8U的变量OSRdyGrp,并使该变量的每一个位都对应OSRdyTbl[]的一个任务组(即数组的一个元素),如果某个任务组又任务就绪,则在变量OSRdyGrp里就把该任务组所对应的位设置为1,否则为0。如下图所示:
这里写图片描述

这里写图片描述

在上图中,创建了优先级最小的两个任务—空闲任务和统计任务。空闲任务的优先级是63,空闲任务就绪,那么OSRdyTbl[7]的最高位为1.统计任务的优先级是62,统计任务也就绪,那么OSRdyTbl[7]的次高位为1。

接下来如果创建优先级别为11任务的话,则就绪表如下:
这里写图片描述

这里有个简单的问题,如何根据任务的优先级别来找到任务在就绪表的位置呢?每个任务的优先级别是唯一的,并且其最大值不会超过63,所以可以把优先级别看成是一个6位的二进制数。这样可以用高3位(D5D4D3)来指明变量OSRdyGrp的具体数据位,并用来确定就绪表数组元素的下标,用低3位(D2D1D0)来指明该数组元素具体数据位。

综上,可以看出uc/OS-II用类似下面的代码把优先级别为prio的任务置为就绪状态:

OSRdyGrp |= OSMapTbl[prio >> 3];
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];

//其中,OSMapTbl[]是uC/OS-II为加快运算速度定义的一个数组,其各元素的值为:
OSMapTbl[0] = 00000001B
OSMapTbl[1] = 00000010B
OSMapTbl[2] = 00000100B
OSMapTbl[3] = 00001000B
OSMapTbl[4] = 00010000B
OSMapTbl[5] = 00100000B
OSMapTbl[6] = 01000000B
OSMapTbl[7] = 10000000B

反之,若要注销某个任务,即系统在就绪表中将该任务的对应位设置为0,则使用如下代码:

if ((OSRbtTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
    OSRbyGrp &= ~OSMapTbl[prio >> 3];

以上代码将任务就绪表OSRbtTbl[]中对应任务的相应位清零,对于OSRbyGrp,只有当被删除的任务所在的任务组全都没有一个处于就绪状态时,才将其相应位清零,即OSRbtTbl[prio >> 3]所有位都为零时,OSRbyGrp相应的位才清零。

最后就是关于最高优先级就绪任务的查找,在说明这个之前先要知道uc/OS-II的几个很重要的概念:
1)uc/OS-II是可抢占实时多任务系统,它总是运行就绪任务中优先级最高的那一个
2)uc/OS-II中不支持时间片轮转法,每个任务的优先级不一样且是唯一的,所以任务调度的工作就是查找准备就绪的优先级最高的任务并进行上下文切换
3)uc/OS-II任务调度所花的时间为常数,与应用程序中建立的任务数无关

根据第3点,uc/OS-II为了找到任务就绪表中优先级最高的任务,不会从OSRbtTbl[0]开始扫描整个任务就绪表,因为如果是这样的话时间就不能确定。所以对此uc/OS-II准备一张优先级判定表,即OSUnMapTbl[],该表定义如下:

NT8U  const  OSUnMapTbl[] = { 
    0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x00 to 0x0F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x10 to 0x1F */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x20 to 0x2F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x30 to 0x3F */
    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x40 to 0x4F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x50 to 0x5F */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x60 to 0x6F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x70 to 0x7F */
    7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x80 to 0x8F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0x90 to 0x9F */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0xA0 to 0xAF */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0xB0 to 0xBF */
    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0xC0 to 0xCF */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0xD0 to 0xDF */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,  /* 0xE0 to 0xEF */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0   /* 0xF0 to 0xFF */
};

首先根据OSRdyGrp查找优先级判定表,原理是根据优先级立即查找到OSRdyTbl[]中对应元素,即OSRdyGrp中从低位到高位第一个为1的位的位置(从0到7)。例如,如果OSRdyGrp为11001000B,那么最低的为1的位是3号位,于是查表得到3,就是说最高优先级OSRdyTbl[3]中。如果是01001000B,同样查找到3,这就是程序中为什么有那么多列是相同的原因。优先级判定表OSUnMapTbl就是根据一个8位的无符号数的数值来确定最低的为1的位的位置的,OSUnMapTbl[n]就是n的最低的为1的位的位号。

了解了OSUnMapTbl[]优先级判定表的作用后,就可以明白uc/OS-II调度器用于获取优先级最高的就绪任务代码如下:

y = OSUnMapTbl[OSRdyGrp];        //获取优先级别的D5、D4、D3位
x = OSUnMapTbl[OSRdyTbl[y]];     //获取优先级别的D2、D1、D0位
prio = (y << 3) + x

y=OSUnMapTbl[OSRdyGrp]是将任务就绪表任务组中最低位为1的位号取出来放在y中,即找到优先级别最高的一组任务组,x = OSUnMapTbl[OSRdyTbl[y]]是将该任务组中优先级别最高的任务找出来。所以最高优先级的任务就可以根据这两点来组成,即prio = (y << 3) + 3.

根据之前的任务就绪表:
这里写图片描述

可以知道OSRbyGrp = 10000010B = 130,根据该值去OSUnMapTbl[]表中找,可得出OSUnMapTbl[130] = 1,即优先级最高的任务组在OSRdyTbl[1],由图可知OSRdyTbl[1] = 00001000B = 8,根据该值去OSUnMapTbl[]表中找,可得出OSUnMapTbl[8] = 3,即在该任务组中,优先级最高的任务是bit3,所以该任务的优先级 (1 << 3) + 3 = 11。

任务调度器代码的设计,即任务就绪表结构以及优先级判定表结构,利用索引查表的方式而不是轮训遍历的方式,使得它的运行时间与系统的任务数无关,从而使其满足了实时系统的要求。这就是uc/OS-II实时操作系统的设计方式之一。

作者:talent_CYJ 发表于2016/10/11 10:18:26 原文链接
阅读:50 评论:0 查看评论

iOS-Main -关于Instruments-Leaks工具的归纳总结

$
0
0

前言: 本篇文章,在于学习,我把别人的一些感觉好的文章汇总成了一篇,亲自实现了一下,留用于今后学习资料。

文章脉络:


文章脉络:
一、内存优化

简介:
Objective_C 有3种内存管理方法, 它们分别是

- MRR (Manual Retain Release, 手动保持释放)
- ARC(Automatic Reference Counting, 自动引用计数)
- GC(Garbage Collection, 垃圾收集)
  • 1>MRR
    ① 也称为 MRC(Manual Reference Counting, 手动引用计数)
    ② 由程序员自己负责管理对象生命周期,负责对象的创建和销毁.
  • 2>ARC
    ① 采用和 MRR 一样的内存引用计数管理方法。
    ② 在编译时会在适合的位置插入对象内存释放, (如 release, autorelease, 和 retain 等),
    ③ 程序员不用关心对象释放的问题, 苹果推荐在新项目中使用 ARC, 但在 iOS5之前的系统中不能采用 ARC.
  • 3>GC
    ① 在Objective_C2.0之后, 内存管理出现了类似于 Java 和 C#的内存垃圾收集技术, 但是垃圾收集与 ARC 一直运行, 垃圾收集是后台有一个线程负责检查已经不再使用的对象,然后释放之.
    ② 由于后台有一个线程一直运行, 一次会严重影响性能, 这也是 Java 和 C#程序的运行速度无法超越 C++的主要原因.
    ③ GC 技术不能应用于 iOS 开发, 只能应用于Mac OS X 开发.

    小结:
    从上面的介绍可知:

  • ① iOS 采用 MRR 和 ARC 这两种方式, ARC 是苹果推荐的方式.
  • ② MRR 方式相对比较原始, 对于程序员的能力要求很高, 但是它很灵活, 方便, 很不容易驾驭好.
二、内存泄露

1> 什莫是内存泄露?
内存泄露指当一个对象或变量在使用完成后没有释放掉, 这个对象一直占用着这部分内存, 直到应用停止.

2> 这种没有 释放掉的对象 多了会发生什么呢?
如果这种对象过多,内存就会耗尽,其他应用就无法运行.

3> 在哪里比较普遍?
这个问题在 C++, C 和 Objective-C的 MRR 中是比较普遍的问题.

4> 理论与实际?
从理论上讲, 内存泄露是由对象或变量没有释放引起的, 但实践证明并非所有的未释放的对象或变量都会导致内存泄露, 这与硬件环境和操作系统系统环境有关。

5> 我们该怎么办呢?
我们需要检测工具帮助我们找到这些"泄漏点".

6> 为什么要测试代码的内存泄露?
内存的泄露导致我们软件在运行过程中占用越来越多的内存,占有资源却又得不到及时清理,会导致我们程序效率越来越低,反应慢,会影响我们用户体验,失去市场的竞争能力.

三、查找泄漏点 (两种工具)

在 Xcode 中, 共提供了两种工具帮助查找泄漏点
1 > Analyze

- 学 名:  静态分析工具
- 查 找:  可以通过 Product ->Analyze 菜单项启动
- 快捷键:  CMD+shift +b.

- Analyze主要分析以下四种问题:
  1) 逻辑错误:访问空指针或未初始化的变量等;
  2) 内存管理错误:如内存泄漏等;
  3) 声明错误:从未使用过的变量;
  4) Api调用错误:未包含使用的库和框架。

2 >Instruments

- 学 名:   动态分析工具
- 查 找:   Product ->Profile 菜单项启动
- 快捷键:  CMD + i.

- 简 介:它有很多跟踪模块可以动态分析和跟踪内存, CPU 和文件系统.
四、两种工具查找漏点版面的介绍

1 > 结合使用-思路分析:
先使用 Analyze 静态分析查找可疑泄漏点, 再用Instruments动态分析中的 Leaks 和 Allocations 跟踪模板进行动态跟踪分析, 确认这些点是否泄漏, 或者是否有新的泄漏点出现等.

2 > 使用 静态检测内存泄漏工具 Analyze 
在 Analyze 静态分析结果中, 凡是有图标


分析结果图标


出现的行都是工具发现的疑似泄露点.


疑似泄漏点所在行


点击疑似泄漏点行末尾的分叉图标,会展开分析结果:


展开分析结果


检测完成时的效果图如下


效果图


小结:
这里使用 Analyze 静态分析查找出来的泄漏点,称之为"可疑泄漏点".之所以称之为"可疑泄漏点",是因为这些点未必一定泄露,确认这些点是否泄露, 还要通过 Instruments 动态分析工具的 Leaks 和 Allocations 跟踪模板. Analyze 静态分析只是一个理论上的预测过程.

3 > 动态监测Instruments的Leaks 
1) CMD + i 打开


打开


2) 打开界面的介绍:


界面的介绍


在 instruments 中,虽然选择了 Leaks 模板,但默认情况下也会添加 Allocations 模板.基本上凡是内存分析都会使用 Allocations 模板, 它可以监控内存分布情况。

① 选中 Allocations 模板,(图1区域),右边的3区域会显示随着时间的变化内存使用的折线图,同时在4区域会显示内存使用的详细信息,以及对象分配情况.

② 点击 Leaks 模板(图中2区域), 可以查看内存泄露情况。如果在3区域有 红X 出现, 则有内存泄露, 4区域则会显示泄露的对象.

3) 打用leaks进行监测:
点击泄露对象可以在(下图)看到它们的内存地址, 占用字节, 所属框架和响应方法等信息.打开扩展视图, 可以看到右边的跟踪堆栈信息


leaks进行监测


4) 监测结果的分析:


监测结果的分析


4 > Allocations—内存分配版面的介绍 
Allocations是检测程序运行过程中的内存分配情况的,也需要同时运行着程序。界面情况如下:


Allocations
五、具体使用

1>.Allocations纪录了内存分配,用来优化内存使用的
2>.Leaks用来分析内存泄漏。ARC中引起的内存泄漏原因就是引用环。

第一步
先选择Leaks和Leaks by Backtrace.这里可以看到那些对象内存泄漏了,泄漏了多少,这个就是简单看看,没有太多调试意义。


泄漏了多少


第二步
然后看看Call Tree,因为Call Tree会给我们大概的位置,有时候会给我们精确的位置,不过要看运气了。
然后,再右面选择Invert Call Tree和Hide System Library


Call Tree


然后双击 上文图片中的任意一行,就会跳到代码处内存泄漏的地方(事实上,到这步,很多内存泄漏的问题都会被发现),当然也有一些泄露还是看不出来的.
第三步
然后我们选择对ARC调试很有用的一个部分Circles & Roots,通过这个我们可以看到详细的ARC引用计数过程。然后,我们看到如下图

小的红色矩形点击可以看到引用计数的详细信息(ARC 就是自动引用计数,计数为0,则对象会被释放)

大的红色矩形可以绘制对象引用环的图,这里如果是我们自己的东西,就能看出来各个对象之间的引用.


Circles & Roots


如果这里没有引用环的图. 首先我们找一下我们自定义的对象,正常完成任务这个对就应该释放的. 为了确认这个对象有没有释放, 可以重写 dealloc 方法, 在此方法中 log 释放信号, 看看是否被释放.

如果这里就是没有释放,我们可以点击这儿对象后的箭头详细的看下, 这个对象的引用计数变化如图.

 - All 表示所有的引用计数变化
 - Unpaired表示那些为成对的变化``(成对就是leaks识别出了对应的+1,-1)
 - By Group会把相关的变化分成一组, 
 - ByTime会按照顺序列出引用计数变化


我们选择Unpaired 和 ByGroup,看到如图



按照顺序看(最左边的标号)
4 这里,引用计数是一,这是正确的,因为到这里正常就是应该是OperationQueue保存一个Operation的引用。 于是,我们把正常的划掉



再继续看,download start 标号6和8是对应的,继续排除问题出现在这里(当然问题不可能出现在这里,这是系统的API,一定会释放,就是简单教大家如何看)



再看看,+1的还剩下标号7 和 11,7 是正常的为Operation分配线程,应当会+1,而11就是我们的问题所在了(大部分Delegate都不会使引用+1)。 我们再看下文档

- > @property(readonly, retain) id< NSURLSessionDelegate > delegate

原来这个代理是retain啊,不是assign或者weak。所以形成了这样的引用环。



那么怎么办呢?有问题下看文档,我们看到图片中引起引用计数加一的是

  - >  + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(id<NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue:

看下文档,发现了这个地方


  • 于是,我们要手动的去断开强引用,于是,我们手动去断开
      -(void)setOperationFinished
      {
       [self.session invalidateAndCancel];
       }
    再运行下看看,能够正常的Dealloc了.

总结:其实大多数问题在双击上文的代码部分就可以解决了,少数问题需要详细的分析ARC引用过程。

建议
如果我们未发现表示内存泄露的红 X, 但是我们想进一步评估某个对象对于内存的应用, 可以看看 Allocations 模板的折线图. 反复执行从创建对象 -> 销毁对象 这个过程, 如果总占用内存数会随之增加, 这说明这个对象没有释放, 有些时候虽然占用的内存不是很严重, 但是也会增加占用内存, 因此必须释放这个对象.

提示:有些情况下, 对象没有释放是无法检测到的,反复测试内存占用也没有明显的增加, 这时最好在配置比较低的设备上测试一下, 如果问题依然, 可以不用释放对象. 但是从编程习惯上讲, 我们应该释放该对象.

事实上,内存泄露是及其复杂的问题, 工具使用是一方面, 经验是另一方面. 提高经验, 然后借助工具才能解决内存泄露的根本.

扩展小知识
1.什莫是内存溢出 out of memory ?

是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

学习文章出处:
1.
 iOS_ 性能优化_内存优化_Leaks工具的使用
2. [学习笔记]_ios内存优化leaks以及timeProfiler工具的使用

作者:qq_33701006 发表于2016/10/11 10:37:25 原文链接
阅读:26 评论:0 查看评论

SportsGo隐私协议

$
0
0

隐私政策

我们的应用名称(以下简称“我们”)制定本隐私政策(以下简称“本政策”),来解释我们如何收集、保存使用由我们的产品、服务和网站(以下合称“我们的服务”)收集的信息。

如果您对本政策有任何问题或投诉,请给我们发邮件到497378690@qq.com

一旦您安装、使用、注册或以其他方式访问我们的服务,那就意味着您已经接受本政策,并且在知情的基础上明确同意按本政策的规定处理、使用和披露您的个人信息。如果您不接受本政策,请不要安装、使用、注册或以其他方式访问我们的服务。我们保留随时修改本政策的权利,并会在修改后于本页面公布修改后的文本。因此,请经常查看本页。如果您继续使用我们的服务,那就意味着您接受对本政策的修改。

1. 非个人信息

在本政策中,“非个人信息”指的是不能直接识别您的信息。此外,非个人信息还指“整合”并“去个人化”的信息,即我们收集的关于我们的服务的使用情况已经去除了可识别个人的数据的信息。

我们可能向合作伙伴和受托人(包括信用卡处理商、邮件服务提供商、运输代理、数据分析商和商业信息提供商等第三方服务提供商,下同)披露并允许其为分析我们的服务使用情况、投放广告、管理或提供我们的服务或改进我们的服务和我们其他的产品和服务而使用您的非个人信息。

您确认并同意我们使用的分析公司可以将通过我们的服务收集的信息与它们独立从其他产品或服务收集的有关于您的活动的其他信息结合起来。这些公司根据自己的政策收集、使用信息。

我们、我们的合作伙伴或受托人可能用从第三方得到的有关人口学、广告、市场和其他分析调查服务的信息补充您的个人和非个人信息。

2. 定位数据

如果您所使用的我们的服务支持定位,我们就可能收集并处理您的定位数据,以提供与您位置相关的服务和广告。例如,有些附加组件或优惠是针对特定地点提供的。我们可能用GPS、Wi-Fi、IP地址等网络数据或其他技术进行定位。未经您同意,我们不会访问您的GPS位置信息。除了我们的合作伙伴提供与定位相关的我们的服务,我们不会将您的GPS位置信息在未经您同意的情况下与第三方共享。如果我们根据本政策与第三方共享GPS位置信息,我们会匿名提供这些信息。请注意,我们可能与我们的合作伙伴匿名共享您的粗略定位信息。

3. 个人信息

本政策中的“个人信息”指的是能直接特定识别您个人的信息。

您可能向我们提供个人信息的情况包括但不限于:(1)注册我们的服务、比赛和特别活动;(2)用社交网站、游戏服务等第三方ID访问我们的服务;(3)订阅新闻通讯;(4)通过我们的网店购买产品或服务;(5)使用“与朋友共享”、“电邮本页”或类似功能;(6)请求技术支持;和(7)使用要求提供个人信息才能使用和/或参加的我们的服务。

本应用可能需要获取你的健康信息等,如果拒绝,我们的应用将有部分功能限制使用,将影响用户体验.

个人信息的类型根据您参加的活动可能不同。我们收集、处理、使用的个人信息可能包括但不限于您的姓名、昵称、在我们的服务或第三方服务中的账户名称、电子邮件地址、电话号码、照片或其他图像、性别、地址、亲友关系、头像、信用卡信息、运输信息和能直接识别您的位置的信息(不能直接识别您的位置信息属于非个人信息)。

如果您选择使用“与朋友共享”或类似服务,为亲友订购礼物,或进行涉及您亲友的其他操作,我们则可能保存您向我们提供的您亲友的姓名、联系方法、地址等信息。

为了分析我们的服务的使用情况、提供客户支持和技术支持、管理和提供我们的服务(包括管理广告投放)、个性化我们的通讯、改进我们的服务、开发我们其他的产品和服务,我们可能单独或整合地使用您的个人信息和非个人信息。我们可能合并个人信息和非个人信息。

我们可能用您的个人信息向您发送技术和/或商业信息,让您了解我们认为您可能感兴趣的我们或第三方的产品和服务,例如新功能、新服务、特别优惠、更新等。

请注意,我们的服务中有些功能为获得与您相关的信息可能会连接到您的社交网站。如果您的社交网站允许并经您许可,我们可能收集您社交网站账户内的信息。此类信息包括但不限于您的姓名、头像、性别、用户名、电子邮件地址、国籍、语言、时区、单位、个人主页链接、您在社交网站上的“亲友”“粉丝”的上述信息以及其他您可能放在社交网站账户中的信息。我们可能会根据本政策的规定使用、结合、关联上述信息。

4. 个人信息的披露和转移

个人信息可能根据法律和本政策的规定披露。此外,我们可能根据本政策向我们的关联人披露个人信息。

我们可能聘请代理人和受托人以我们的名义收集和处理个人信息。在这种情况下,我们会指示代理人和受托人遵守本政策,只为所聘请的目的使用个人信息。我们可能聘用信用卡处理商、邮件服务提供商、运输代理、数据分析商和商业信息提供商等第三方服务提供商。我们可以在必要的情况下与上述第三方共享您的个人信息,使其能够为我们服务。除非法律强制规定,我们不为第三方的作为或不作为承担责任。

我们可能为满足执法部门或政府官员在调查欺诈、知识产权侵权或其他违法或可能使我们承担法律责任的活动时按其要求向第三方披露您的个人信息。我们向第三方披露您的个人信息还可能发生在我们合理地认为需要披露才能处理实际或可能侵害我们的权利、财产、经营、用户和其他可能受损方时,或者是在我们认为保护我们的权利、打击欺诈、遵守适用于我们的法律规定和国家命令需要披露时。如果法律允许披露,我们将尽合理努力通过我们的网站或以其他合理方式通知您相关的披露。

虽然我们不会向第三方出售您的电子邮件、电话号码或地址,但我们将用户输入或从使用情况推断的数据整合起来,在不会识别个人的前提下自己利用或向第三方提供整合数据。

5. 数据的保存和纠正

我们根据本政策保存数据至本政策规定的目的实现时为止,除非法律要求或允许保存更长时间。之后,如果为本政策规定的目的不再需要,我们可能在合理时间内完全删除我们所保存的数据。我们不去核实个人信息是否正确。

虽然有上述规定,我们仍可能保存某些解决争议、执行用户协议、满足技术和法律要求和维护我们的服务的安全完整运行所需的数据。

6. cookies、网络信标和跟踪

我们的服务可能使用cookies和像素标签(pixel tags)、本地共享对象、网络信标(clear GIFs)和网络信标(web beacons)等技术。我们将cookies和类似技术收集来的信息当做非个人信息来处理。

cookies:“cookies”是网站经常存储于用户电脑上的少量记录信息。我们的cookies不会包括个人信息,一般用于快速识别您的设备并“记住”您。您可以取消cookies,或把您的浏览器设置成为向您发送cookies就会提示您的模式。但取消cookies可能会影响您使用我们的服务。

Flash cookies和HTML5:我们可能使用Flash cookies(本地共享对象)和HTML5(本地存储对象)。本地共享对象是类似于浏览器cookies的小文件,用于记忆您的设置,从而个性化我们的服务的外观。本地共享对象只以整合的方式收集数据。您可以通过在您的浏览器取消本地存储对象或访问www.adobe.com来阻止安装本地共享对象。HTML5 Web Storage等本地存储对象与cookies作用类似,但一般比浏览器cookies的信息量更大、更多样。

网络信标和像素标签:“网络信标”或“像素标签”是让我们能计算网页访问数量或广告浏览量的电子图像。类似于cookies,网络信标不包含个人信息。我们向您发送的电子邮件等电子通讯可能包含像素标签或网络标签,使我们能追踪通讯的使用情况,如是否打开过通讯、点击了哪个链接等。

7. 第三方的条款和条件

请注意,您在使用和访问我们的服务时,可能还要遵守应用商店、地图提供商、移动软件平台、网游平台、社交网站和支付中介等某些第三方的条款和条件以及隐私政策。您确认并同意,我们无需为这些第三方的条款和条件以及第三方如何使用您的个人信息承担责任。

我们可以自主选择通过广告或其他形式让您能链接到第三方产品或服务。请注意,您所用的第三方产品和服务不是由我们所关联或控制的人和企业开发和管理的。我们不为这些人或企业的行为、产品和服务以及如何使用您提供的信息负责。我们提供它们的链接并不能让我们与其发生关联或控制关系。

我们可能随时提供我们合作伙伴的服务,例如竞猜、调查等。它们的服务可能会要求您提供个人信息才能注册或访问。这些服务将会在您需要披露个人信息时标明合作伙伴的身份。如果您选择披露个人信息,那么这些数据则可能直接或通过我们间接提供给第三方。您将要接受它们这些第三方的隐私政策和做法。我们不为这些第三方的隐私政策和做法负责。因此,您应该在披露个人信息前审阅它们的隐私政策和做法。

请注意,在我们的服务中,多人对战、购物、社交、游戏等有些项目可能用第三方服务与游戏身份、网购账户、社交账户、网络账户等信息对比验证。如果您选择参加或使用这些服务,则可能自动会向这些第三方传送某些个人用户或账户数据。您在此同意我们根据本政策处理、使用、整合、披露并保存这些数据。

8. 保障措施

我们按照行业通行标准合理保护信息的安全、完整和秘密。您个人信息的访问权限仅限于为根据本政策完成工作任务或进行技术维护工作需要处理您个人信息的人员。我们收集的个人信息保存于不向公众开放的安全运行环境之中。为避免未经授权访问您的个人信息,我们将其保存在有防火墙保护并可能加密的服务器之中。但是,没有什么系统是100%安全的。即使我们做出了合理努力,仍可能有人未经允许访问您的个人信息。此外,人们的言行可能不可靠,或具有误导性、非法性。我们无法告诉您别的用户说的是否属实。如果您使用我们的服务,您要自担以上数据失窃或被误导、被欺诈、被违法侵害的风险。
如果您不接受本政策、提供我们要求提供的信息,那么您可能无法完全或部分使用我们的服务和我们提供的功能。但您需要了解,如果您提供了这些信息,您将承担这些信息被窃取、非法占有、滥用的风险。如果出现了上述情况,您的安全、财产和声誉可能受损。

9. 其他

请您了解,社交网络和我们的服务可能具有开放性。您选择在创建内容的过程中向我们的服务披露关于您的信息。任何您在论坛、博客、聊天等环境下披露的数据都可能成为公开信息,不可期待隐私或保密。我们对您选择在以上场合披露的任何个人信息都不承担责任。

如果您在您的居住地被认为是未成年人,请您取得监护人同意后再使用或访问我们的服务。我们鼓励家长和/或监护人积极辅导孩子的网上活动。如果用户在其居住地被认为是未成年人,我们不会在明知的情况下收集他们的信息。如果我们了解到不经意间收集了他们的个人信息,我们会采取合理措施立刻从我们的记录中删除他们的个人信息。

我们可能根据法律和本政策规定保存和/或向海外的关联人和合作伙伴披露您的个人信息。我们可能在与企业并购、重组、出售全部或部分股票和/或资产等重大变化相关的情况下,包括但不限于尽职调查过程中,向第三方披露您的个人信息,但本政策将继续适用于这些个人信息。


作者:yj55555 发表于2016/10/11 10:42:41 原文链接
阅读:27 评论:0 查看评论

Bluetooth profile: ATT/GATT

$
0
0

英文原文URL:https://epx.com.br/artigos/bluetooth_gatt.php

因为看了这篇英文文档介绍Bluetooth ATT/GATT很详细,所以翻译了一下,供参考!

蓝牙4.0版本推出了低功耗规范,引入了两个核心协议:ATT(Attribute Protocol)和GATT(Generic Attribute Protocol).这两个协议主要目标是BLE,但是也可以运行在传统蓝牙上(BR/EDR).

Overview

ATT是wire application protocol(无线应用层连接协议?),GATT基于ATT协议。所有的BLE profile一定基于GATT。也就是所有的BLE服务都使用ATT作为应用协议。

锁定BLE使用这两个协议的好处是:

1, 开发和实现新的BLE profile更加容易,因为不需要从头实现wire protocol。

2, ATT针对BLE 设备进行了特别的优化:使用尽可能少的字节,因此可能在存储中使用定长结构来生成PDU。

3, ATT/GATT的简单意味着固件可能提供某种程度的协议支持,省去了微处理器软件的麻烦。

4, 对于软件实现的协议栈来说,ATT/GATT在协议栈里实现,省去了应用的麻烦。

5, 即使有的场景下,ATT/GATT不够理想。也可以在L2CAP连接上实现平行于ATTchannel的协议。

ATT: Attribute Protocol

ATT协议的唯一基础是属性。每个属性由三个元素构成:

1,一个16bit handle;

2,一个UUID来定义属性的类型;

3,确定长度的属性值

在ATT中,属性值可以是任意长度的byte数组。属性值的实际意义依赖于UUID,而且ATT并不会检查属性值长度是否与给定的UUID定义一致。

Handle是用来唯一识别属性的数字,因为在一个BLE 设备中可能存在多个属性具有相同的UUID。

ATT协议本身没有定义任何UUID。这部分工作留给了GATT和上层协议。

ATT server存储属性。ATT client什么也不存储,它使用ATT协议来读写server端的属性。

和属性相关的还有读写权限。读写权限存在属性值里,由高层协议确定。ATT本身不会关心,也不会试图解释属性值来确定权限。这部分工作也留给了GATT和上层协议。

ATT有一些良好的特征,比如通过UUID来搜索属性,通过handle区间范围来获取所有区间内的属性,因此client不需要提前获得handle的值,也不需要高层协议硬编码这些值。

但是在特定的设备上handle的取值最好保持不变,这样的话client能够缓冲信息。在第一个discovery以后,client能够使用缓冲信息,这样能够减少传输的包数量,也能够节约能量。如果服务端的属性布局已经发生了变换,高层协议应该能够”暗示”client,比如固件升级。

大多数情况下ATT协议都是纯C/S架构,client发起请求,server响应。但是服务端也有通知的能力,在服务端属性发生变化时,server能够通知client,这样避免了client不停的poll。

ATT协议不会显式发送属性值的长度,只能从PDU长度里面获得。因此client最好能够知道某种UUID类型所代表的属性的精确结构。

不发送属性值长度,是为了减少发送的字节,因为LE的MTU只有23bytes。

23bytes的MTU对于较长的属性值来说是个麻烦。因此不得采用“read long”,”write long“这样的操作。

ATT是如此通用,意味着高层协议有太多工作要做。过度的自由也会带来问题,比如:如果一个设备提供多个服务怎么办?对每一个设备只有一个ATT handle空间,多个服务不得不共享同一份空间。

幸运地是,我们还有GATT,它为我们提供了属性用法,并解除了这些限制。

GATT:Generic Attribute Profile

GATT是所有LE顶层协议的基础。它定义了怎么把一堆ATT属性分组成为有意义的服务。


GATT services

GATT service的基础是UUID值为0x2800的属性。所有跟在这个属性后面的属性都属于这个属性定义的服务,直到另一个0x2800属性出现。

比如说,一个设备里面的三个属性布局如下: 

Handle UUID Description
0x0100 0x2800 Service A definition
... ... Service details
0x0150 0x2800 Service B definition
... ... Service details
0x0300 0x2800 Service C definition
... ... Service details

 

每一个属性不知道它自己属于哪个服务,GATT需要根据0x2800属性作为标记来识别出哪个属性属于哪个服务。

按照这个定义,handle值就有意义了。在上面的例子中,属于service B的属性handle必须位于0x0151和0x02ff之间。

UUID 0x2800定义了primary服务,

也可以使用0x2801来定义secondary服务。

Secondary服务表示包含于primary服务。

然后我们怎么能知道一个服务是温度计,智能钥匙或者GPS?答案是通过读取属性值。服务属值包含了一个UUID,通过这个UUID区分服务。

因此,每个属性定义事实上包含了两个UUID,0x2800或者0x2801作为属性UUID,另外一个属性值里面存储的UUID。后面这个UUID是服务ID。

举个例子: 

Handle UUID Description Value
0x0100 0x2800 Thermometer service definition UUID 0x1816
... ... Service details ...
0x0150 0x2800 Service B definition 0x18xx
... ... Service details ...
0x0300 0x2800 Service C definition 0x18xx
... ... Service details ...

在图中, thermometer service的UUID是0x1816。

是不是感觉怪怪的?两个UUID定义一个服务?这是GATT/ATT分层方式导致的后果。

UUID0x2800被GATT用来寻找服务定义边界。一旦找到了边界,属性值,也就是第二个UUID用来指定服务。这样client能够找到所有的服务而不需要知道服务的具体定义。

GATT service characteristics

每一个服务有几个特征。特征存储了有用的值以及权限。

比如,一个温度计可能有只读的温度特征,也可能有可读写的时间戳。

 

Handle UUID Description Value
0x0100 0x2800 Thermometer service definition UUID 0x1816
0x0101 0x2803 Characteristic: temperature UUID 0x2A2B
Value handle: 0x0102
0x0102 0x2A2B Temperature value 20 degrees
0x0110 0x2803 Characteristic: date/time UUID 0x2A08
Value handle: 0x0111
0x0111 0x2A08 Date/Time 1/1/1980 12:00

 

每一个服务可能有几个特征,这些特征也是通过路碑属性来发现的。

主特征的UUID是0x2803,然后主特征的属性值用来定义特征。比如图中 0x2803用来找到特征,0x2A2B用来找到特征包含的信息。

每一个特征至少包含两个属性,主属性0x2803和真正的值属性。主属性知道属性值的handle和UUID。这能够进行一定程度的交叉检测。

特征值的真正格式是由UUID决定的。因此,如果客户端知道如何解释UUID为 0x2A08的特征值,就能够从包含这个特征任何服务里面读取日期和时间。当然如果客户端不知道如何解释这个UUID的话,也可以选择忽略。

Characteristic descriptors

   除了特征值,我们也可以为每个特征增加更多的属性。在GATT语法里,这个额外的属性成为描述符。

         举个例子子,我们也许需要指定温度的计量单位。 

Handle UUID Description Value
0x0100 0x2800 Thermometer service definition UUID 0x1816
0x0101 0x2803 Characteristic: temperature UUID 0x2A2B
Value handle: 0x0102
0x0102 0x2A2B Temperature value 20 degrees
0x0104 0x2A1F Descriptor: unit Celsius
0x0110 0x2803 Characteristic: date/time UUID 0x2A08
Value handle: 0x0111
0x0111 0x2A08 Date/Time 1/1/1980 12:00


GATT知道handle 0x0104是特征0x0101的描述符,因为:

1, 他不是特征的值,因为特征值的handle应该是0x0102

2, 他的handle落在了0x0103-0x010f之间,因此也不属于下一个特征。

 

描述符值的意义依赖于属性UUID。例子中,描述符的UUID是0x2A1F,客户端如果不能识别这个UUId,他可以选择忽略。这样可以实现向下兼容。

每个服务可能定义自己的描述符,但是GATT已经定义了能够覆盖大多数情况的标准描述符,比如:

数值格式和表示;

人类可读的描述;

合理范围扩展属性等等。其中特别重要的描述符是client characteristic configuration。

Client Characteristic Configurationdescriptor

Client Characteristic Configurationdescriptor的UUID是0x2902,具有一个16bit的可读写值,作为一个bitmap来使用。

这个属性被server用来存储和代表每个已经绑定的client的独立实例,每个client只能看到它自己的拷贝。

前两个bit被GATT用来定义通知和暗示。其他bit暂时未使用。

通过设置CCC,client能够让server在特征发生改变时得到通知。比如包含了CCC的属性布局如下: 

Handle UUID Description Value
0x0100 0x2800 Thermometer service definition UUID 0x1816
0x0101 0x2803 Characteristic: temperature UUID 0x2A2B
Value handle: 0x0102
0x0102 0x2A2B Temperature value 20 degrees
0x0104 0x2A1F Descriptor: unit Celsius
0x0105 0x2902 Client characteristic configuration descriptor 0x0000
0x0110 0x2803 Characteristic: date/time UUID 0x2A08
Value handle: 0x0111
0x0111 0x2A08 Date/Time 1/1/1980 12:00


Servicediscovery in Low Energy

因为GATT中所有的服务细节通过ATT来描述,所以不需要像BR/EDR那样设置专门的服务发现协议。ATT负责一切:发现服务,查找特征,读写值等等。

GATT andvanilla Bluetooth

GATT也可以工作在传统蓝牙上面,但是规范规定传统蓝牙仍然使用SDP发送服务,即使通过GATT来进行实际数据交换。

这样的好处是在双模设备上不用设置标识来识别LE-only服务。如果一个服务只能通过GATT发现,就是LE-only。如果能够通过GATT和SDP发现,就是双模。

         如果一个profile通过GATT来进行数据交换,并且是双模的,它必须首先发布SDP record。然后这个服务通过SDP来发现,然后通过GATT来查找特征。

         当然,现在没有双模的profile。以前的profile是BR/EDR only,并且没有适配到GATT;LE-only只有LE。

         如果想要测试GATT而没有LE硬件,可以修改蓝牙协议栈来使BR/EDR可以进行GATT discovery。这是规范不运行的,但是开发者可以。

Notificationsversus connections

        通知和暗示使得server可以发送消息给client。这样客户端不需要pollserver来获取新的数据。

另外,典型的GATT server是“小的“外设,像非常需要节能的传感器之类。因此,外设的LE 设备不能发起连接。那么通知怎么发送呢?

在BLE协议栈,如果server有数据发送,它就进入广播模式,并且发送一些信号。每个profile定义了广播时长和频率。时长和频率应该根据使用场景进行了节能和及时性的权衡。

处于中心模式的设备随时处于监听模式。当它监听到广播后,如果发现广播设备是认识的(配对过或者白名单中的),就会向外设发起连接。

连接建立以后,GATT通信能够进行,通知得以发送。所以典型的序列是:1,server发送广播 2,client连接 3server通知

如果没有更多的数据发送,server和client就会超时断开。最佳超时时间依赖于用例;如果服务不会频繁发送通知并且没有实时性要求的话,可以立马断开。因为BLE重连是非常快的。

典型的GATT server是外设设备,但是不是必须的。也可以外设做client,center做server。在这种场景下,client想要读写数据的时候,需要先进入广播模式。

作者:zwc1725 发表于2016/10/11 10:49:17 原文链接
阅读:24 评论:0 查看评论

仿QQ头像自定义截取功能

$
0
0

看了android版QQ的自定义头像功能,决定自己实现,随便熟悉下android绘制和图片处理这一块的知识。

先看看效果:
这里写图片描述

思路分析:

这个效果可以用两个View来完成,上层View是一个遮盖物,绘制半透明的颜色,中间挖了一个圆;下层的View用来显示图片,具备移动和缩放的功能,并且能截取某区域内的图片。

涉及到的知识点:

1.Matrix,图片的移动和缩放
2.Paint的setXfermode方法
3.View的draw方法

编码实现:

自定义三个View:
1.下层View:ClipPhotoView
2.上层遮盖View:ClipPhotoCircleView
3.布局文件:ClipPhotoLayout,实现两层View的布局,且作为整个功能的facade

ClipPhotoCircleView代码:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawMask(canvas);
    }

    /**
     * 绘制蒙版
     */
    private void drawMask(Canvas canvas) {
        //画背景颜色
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas c1 = new Canvas(bitmap);
        c1.drawARGB(150, 0, 0, 0);
        Paint strokePaint = new Paint();
        strokePaint.setAntiAlias(true);
        strokePaint.setColor(Color.WHITE);
        strokePaint.setStyle(Paint.Style.STROKE);
        strokePaint.setStrokeWidth(STROKE_WIDTH);
        c1.drawCircle(getWidth() / 2, getHeight() / 2, getRadius(), strokePaint);

        //画圆
        Bitmap circleBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas c2 = new Canvas(circleBitmap);
        Paint circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.FILL);
        circlePaint.setColor(Color.RED);
        circlePaint.setAntiAlias(true);
        c2.drawCircle(getWidth() / 2, getHeight() / 2, getRadius(), circlePaint);
        //两个图层合成
        Paint paint = new Paint();
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        c1.drawBitmap(circleBitmap, 0, 0, paint);
        paint.setXfermode(null);

        canvas.drawBitmap(bitmap, 0, 0, null);
    }

使用了setXfermode,Mode为DST_OUT,如下图:

这里写图片描述

ClipPhotoView代码:

/**
 * Created by caocong on 10/9/16.
 * 显示图片的view,可以托动和缩放
 */
public class ClipPhotoView extends ImageView implements View.OnTouchListener,
        ScaleGestureDetector.OnScaleGestureListener {

    private static final String TAG = ClipPhotoView.class.getSimpleName();
    //最大缩放比例
    private static final float MAX_SCALE = 4.0f;
    //最小缩放比例
    private static float MIN_SCALE = 1.0f;
    //matrix array
    private static final float MATRIX_ARR[] = new float[9];

    /**
     * 状态
     */
    private static final class Mode {
        // 初始状态
        private static final int NONE = 0;
        //托动
        private static final int DRAG = 1;
        //缩放
        private static final int ZOOM = 2;
    }

    //当前状态
    private int mMode = Mode.NONE;
    //缩放手势
    private ScaleGestureDetector mScaleDetector;
    //矩阵
    private Matrix mMatrix = new Matrix();

    //托动时手指按下的点
    private PointF mPrevPointF = new PointF();

    //截取的圆框的半径
    private int mRadius;

    //第一次
    private boolean firstTime = true;

    public ClipPhotoView(Context context) {
        this(context, null);
    }

    public ClipPhotoView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ClipPhotoView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScaleDetector = new ScaleGestureDetector(context, this);
        mRadius = Util.getRadius(getContext());
        // 必须设置才能触发
        setOnTouchListener(this);
        setScaleType(ScaleType.MATRIX);
    }

    /**
     * 初始化
     */
    private void init() {
        Drawable drawable = getDrawable();
        if (drawable == null) {
            //throw new IllegalArgumentException("drawable can not be null");
            return;
        }
        initPosAndScale();

    }


    /**
     * 初始化缩放比例
     */
    private void initPosAndScale() {
        if (firstTime) {
            Drawable drawable = getDrawable();
            int width = getWidth();
            int height = getHeight();
            //初始化
            int dw = drawable.getIntrinsicWidth();
            int dh = drawable.getIntrinsicHeight();

            float scaleX = 1.0f;
            float scaleY = 1.0f;
            //是否已经做过缩放处理
            boolean isScaled = false;
            if (width < getDiameter()) {
                scaleX = getDiameter() * 1.0f / width;
                isScaled = true;
            }
            if (height < getDiameter()) {
                scaleY = getDiameter() * 1.0f / height;
                isScaled = true;
            }
            float scale = Math.max(scaleX, scaleY);
            if (isScaled) {
                MIN_SCALE = scale;
            } else {
                MIN_SCALE = Math.max((getDiameter() * 1.0f) / dw, getDiameter() * 1.0f / dh) + 0.01f;
            }
            Log.d(TAG, "scale=" + scale);
            mMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
            mMatrix.postTranslate((width - dw) / 2, (height - dh) / 2);
            setImageMatrix(mMatrix);
            firstTime = false;
        }

    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();
        if ((scale >= MIN_SCALE && scaleFactor > 1.0f) ||
                (scale <= MAX_SCALE && scaleFactor < 1.0f)) {
            if (scale * scaleFactor <= MIN_SCALE) {
                scaleFactor = MIN_SCALE / scale;
            } else if (scale * scaleFactor >= MAX_SCALE) {
                scaleFactor = MAX_SCALE / scale;
            }
            mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
            checkTrans();
            setImageMatrix(mMatrix);
        }

        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        mMode = Mode.ZOOM;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        mMode = Mode.NONE;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (getDrawable() == null) {
            return false;
        }

        mScaleDetector.onTouchEvent(event);

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                mMode = Mode.DRAG;
                mPrevPointF.set(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_UP:
                mMode = Mode.NONE;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mMode == Mode.DRAG && event.getPointerCount() == 1) {
                    float x = event.getX();
                    float y = event.getY();
                    float dx = event.getX() - mPrevPointF.x;
                    float dy = event.getY() - mPrevPointF.y;
                    RectF rectF = getMatrixRectF();
                    // 如果宽度小于屏幕宽度,则禁止左右移动
                    if (rectF.width() <= getDiameter()) {
                        dx = 0;
                    }
                    // 如果高度小雨屏幕高度,则禁止上下移动
                    if (rectF.height() <= getDiameter()) {
                        dy = 0;
                    }
                    mMatrix.postTranslate(dx, dy);
                    checkTrans();
                    //边界判断
                    setImageMatrix(mMatrix);
                    mPrevPointF.set(x, y);
                }
                break;
        }
        return true;
    }

    /**
     * 移动边界检查
     */
    private void checkTrans() {
        RectF rect = getMatrixRectF();
        float deltaX = 0;
        float deltaY = 0;

        int width = getWidth();
        int height = getHeight();

        int horizontalPadding = (width - getDiameter()) / 2;
        int verticalPadding = (height - getDiameter()) / 2;

        // 如果宽或高大于屏幕,则控制范围 ; 这里的0.001是因为精度丢失会产生问题
        if (rect.width() + 0.01 >= getDiameter()) {
            if (rect.left > horizontalPadding) {
                deltaX = -rect.left + horizontalPadding;
            }
            if (rect.right < width - horizontalPadding) {
                deltaX = width - horizontalPadding - rect.right;
            }
        }
        if (rect.height() + 0.01 >= getDiameter()) {
            if (rect.top > verticalPadding) {
                deltaY = -rect.top + verticalPadding;
            }
            if (rect.bottom < height - verticalPadding) {
                deltaY = height - verticalPadding - rect.bottom;
            }
        }
        mMatrix.postTranslate(deltaX, deltaY);
    }


    /**
     * 得到直径
     */
    public int getDiameter() {
        return mRadius * 2;
    }

    /**
     * 获得缩放值
     *
     * @return
     */
    private float getScale() {
        return getMatrixValue(Matrix.MSCALE_X);
    }


    private float getMatrixValue(int index) {
        mMatrix.getValues(MATRIX_ARR);
        return MATRIX_ARR[index];
    }


    /**
     * 获得Matrix的RectF
     */
    private RectF getMatrixRectF() {
        Matrix matrix = mMatrix;
        RectF rect = new RectF();
        Drawable d = getDrawable();
        if (null != d) {
            rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
            matrix.mapRect(rect);

        }
        return rect;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        init();

    }

    /**
     * 截取图片
     *
     * @return
     */
    Bitmap clip() {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        draw(canvas);
        int x = (getWidth() - getDiameter()) / 2;
        int y = (getHeight() - getDiameter()) / 2;
        return Bitmap.createBitmap(bitmap, x, y, getDiameter(), getDiameter());
    }

}

缩放和移动使用了Matrix的方法postScale()和postTranslate,要注意控制边界。
截图的代码在clip()方法中,原理:新建一个空白Bitmap,和屏幕一样大的尺寸,然后将当前View绘制的内容复制到到这个Bitmap中,然后截取该Bitmap的一部分。

ClipPhotoLayout代码:

public class ClipPhotoLayout extends FrameLayout {
    private ClipPhotoCircleView mCircleView;
    private ClipPhotoView mPhotoView;

    public ClipPhotoLayout(Context context) {
        this(context, null);
    }

    public ClipPhotoLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ClipPhotoLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();


    }

    private void init() {
        mCircleView = new ClipPhotoCircleView(getContext());
        mPhotoView = new ClipPhotoView(getContext());

        android.view.ViewGroup.LayoutParams lp = new LinearLayout.LayoutParams(
                android.view.ViewGroup.LayoutParams.MATCH_PARENT,
                android.view.ViewGroup.LayoutParams.MATCH_PARENT);
        addView(mPhotoView, lp);
        addView(mCircleView, lp);

    }

    public void setImageDrawable(Drawable drawable) {
        mPhotoView.setImageDrawable(drawable);
    }

    public void setImageDrawable(int resId) {
        setImageDrawable(getContext().getDrawable(resId));
    }

    public Bitmap clipBitmap() {
       return mPhotoView.clip();
    }
}

测试MainActivity:

public class MainActivity extends Activity {
    private ClipPhotoLayout mClipPhotoLayout;
    private int[] pictures = {R.drawable.mingren, R.drawable.cute, R.drawable.tuxi};


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.scale);
        setTitle("移动和缩放");
        mClipPhotoLayout = (ClipPhotoLayout) findViewById(R.id.clip_layout);
        mClipPhotoLayout.setImageDrawable(pictures[0]);
    }

    public void doClick(View view) {
        Bitmap bitmap = mClipPhotoLayout.clipBitmap();
        Intent intent = new Intent(this, ResultActivity.class);
        intent.putExtra("photo", bitmap);
        startActivity(intent);
    }


}

MainActivity的布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.caocong.image.widget.ClipPhotoLayout
        android:id="@+id/clip_layout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1.0"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="doClick"
        android:text="clip" />

</LinearLayout>

源码下载地址

作者:cao12345cong 发表于2016/10/11 10:50:32 原文链接
阅读:40 评论:0 查看评论

小艾笔记--H.264视频编解码原理整理(三)

$
0
0

昨天介绍了视频编解码的原理,内容实属困难啊!脑细胞死一片。。。今天来点简单的,写完就去吃午饭!

  • 主流的视频编码算法
  • MPEG-4和H.264区别
  • H.264特点
  • 视频解码的原理及主流解码器
  • 解码原理
  • 专用芯片型和可编程型特点
  • 开源的视频编解码器(CODEC)

主流视频编码算法

编码算法具有高计算量和受实现平台的影响等特点,所以技术一直在不断地完善,主流是MPEG-4和H.264。而且市场需求量也是很大的,但出现一个问题:就是每个领域都有几家特别牛逼的几家公司在哪里,人家大而不倒,你想跻身进入500强,没有强悍的身板(高质量的技术),你怕站不住啊!

MPEG-4和H.264区别

MPEG-4是ISO组织制定的音视频编解码算法,主要针对网络、视频会议和可视电话等低码率传输应用;

H.264是ITU-T和ISO联合组织JVT制定的视频编解码标准,在MPEG-4中是Part-10,称为AVC(Advanceel Video Ccoding),在ITU中称为H.264。

H.264特点

H.264算法还是基于块的混合编码技术,编码过程基本与以前的编码标准相同,只是每个功能模块都进行了技术更新,帧内预测、帧间预测、整数DCT变换、环路滤波、熵编码等模块都做了技术提升。

(1)NAL与VCL

网络适配层NAL(Network Abstraction Layer)是H.264为适应网络传输应用而制定的一层数据打包操作。传统的视频编码算法编完的视频码流在任何应用领域下(无论用于存储、传输等)都是统一的码流模式,视频码流仅有视频编码层VCL(Video Coding Layer)。而H.264可根据不同应用增加不同的NAL片头,以适应不同的网络应用环境,减少码流的传输差错。

(2)帧内预测

H.264为能进一步利用图像的空间相关性,H.264引入了多模式的帧内预测以提高压缩效率。简单地说,帧内预测编码就是用周围邻近的像素值来预测当前的像素值,然后对预测误差进行编码。预测是基于块的,亮度分量(Luma)块的大小可以在16×16和4×4之间选择,16×16块有4种预测模式,4×4块有9种预测模式;色度分量
(Chroma)预测是对整个8×8块进行的,预测模式同亮度16×16的4种预测模式。

(3)帧间预测

帧间预测即传统的运动估计ME加运动补偿MC,H.264的运动估计更精准、快速,效果更好。

1)多变的宏块大小

传统的运动估计块大小是16×16,由于运动物体复杂多变,仅使用一种模式效果不好。H.264采用了7种方式对一个宏块进行分割,分别为16×16、16×8、8×16、8×8、8×4、4×8、4×4,每种方式下块的大小和形状都不相同,这就使编码器可以根据图像的内容选择最好的预测模式。实验表明,与仅使用16×16块进行预测相比,使用不同大小和形状的块可以使码率节省15%以上。

2)更精细的像素精度

在H.264算法中,Luma分量的运动矢量MV使用1/4像素精度。Chroma分量的MV由Luma MV导出,由于Chroma分辨率是Luma的一半(YUV4:2:0),所以其MV精度将为1/8。如此精细的预测精度较之整数精度可以使码率节省超过20%。

3)更多参考帧

H.264支持多参考帧预测(Multiple Reference Frames),即可以有多于1个(最多5个)的在当前帧之前解码重建的帧,作为参考帧产生对当前帧的预测(Motion-compensated Prediction)。这特别适用于视频序列中含有周期性运动的情况。

4)环路滤波

环路滤波(Loop Filter)的作用是,消除经反量化和反变换后重建图像中由于预测误差产生的块效应,从而一方面改善图像的主观质量,另一方面减少预测误差。与以往的Deblocking Filter不同的是,经过滤波后的图像将根据需要放在缓存中用于帧间预测,而不是仅仅在输出重建图像时用来改善主观质量,也就是说该滤波器位于解码环中,而非解码环的输出外,因而得名Loop Filter。

(4)整数DCT变换

传统的DCT是由浮点算法定点实现,所以IDCT不是可逆的,容易造成解码图像的周围“拖尾”现象。H.264对帧内或帧间预测的残差(Residual)进行整数DCT变换编码。新标准对DCT的定义做了修改,使得变换仅用整数加减法和移位操作即可实现,这样在不考虑量化影响的情况下,解码端的输出可以准确地恢复编码端的输入。此外,该变换是针对4×4块进行的,这也有助于减少块效应。为了进一步利用图像的空间相关性,在对色度(Chroma)的预测残差和16×16帧内预测的预测残差进
行上述整数DCT变换之后,标准还将每个4×4变换系数块中的DC系数组成2×2或4×4大小的块,进一步做哈达玛(Hadamard)变换。

(5)熵编码

对于预测残差,H.264有两种熵编码的方式:基于上下文的自适应变长码
CAVLC(Context-based Adaptive Variable Length Coding)和基于上下文的自适应二进制算术编码CABAC(Context-based Adaptive Binary Arithmetic Coding);如果待编码的数据不是预测残差这一类型,则H.264采用Exp-Golomb码或CABAC来编码,具体选用哪种编码类型视编码器的设置而定。

1)CAVLC

可变字长编码VLC的基本思想就是,对出现频率大的符号使用较短的码字,而出现频率小的符号采用较长的码字,这样可以使平均码长最小。在CAVLC中,H.264采用若干VLC码表,不同的码表对应不同的概率模型。编码器能够根据上下文,如周围块的非零系数或系数的绝对值大小,在这些码表中自动地选择,最大可能地与当前数据的概率模型匹配,从而实现了上下文自适应的功能。

2)CABAC

算术编码是一种高效的熵编码方案,其每个符号所对应的码长被认为是分数。由于每一个符号的编码都与以前编码的结果有关,所以它考虑的是信源符号序列整体的概率特性,而不是单个符号的概率特性,因而它能够更大程度地逼近信源的极限熵,极大的降低码率。

视频解码的原理及主流解码器

根据编码的过程,解码就是编码的逆操作。但在编码的操作中已经有所体现,对于MPEG-4视频编解码算法来说,在图像或残差做DCT变换、量化后,接着是反量化、IDCT变换,然后将重建的数据补偿到编码图像中,从而保证解码时数据不会产生偏差。而实际的解码器只是增加了熵解码的操作,后续的处理与编码器中的图像帧重建是相同的。

解码原理

首先解析码流的头数据,获取编码图像的有关参数,包括帧编码类型(I/P)、图像宽度或高度等,后续就是以宏块为单位循环解码,图中的阴影框表示以宏块为处理单元循环执行。熵解码是可变长编码VLC的逆操作,即VLD。H.263/MPEG-1/2/4是Huffman熵解码,即通常意义上的VLD,而H.264则是采用了算术解码,又包括CAVLD、CABAD。另外,
对于帧间编码的宏块,解码器还要解析出当前宏块的运动向量。熵解码后是反量化操作,反量化就是量化结果乘以量化步长,对于不同的解码算法又有不同的反量化处理,H.263采用了32级的均匀量化,即宏块数据采取一个量化步长;MPEG-4除了支持H.263的均匀量化外,还增加了量化表的处理方式;H.264采用了52级的均匀量化方式。反量化处理后,进行反变换IDCT,对H.263/MPEG-1/2/4采取了8×8块的浮点式IDCT,H.264采取了4×4的整数ICT。运动补偿是解码器中的重点,占用了约60%以上的计算负荷,这是因为码流统计中帧间编码为主要的编码类型,而与之对
应的处理就是插值运动补偿,根据从码流中解析的运动向量信息,定位参考帧的确切位置,然后计算1/2、1/4像素精度的插值,最后把结果补偿(加)到重建帧中。解码器中的最后处理是可选的去除块效应(MPEG-4)、环路滤波(H.264)、图像扩展等。

解码原理图

之前说过,MPEG-4/H.264标准协议给出的只是一个框架或码流的语义,并没有统一的标准对于如何实现编码器或者解码器。所以借助芯片,从实现平台或可升级等特点,主要有专用芯片型和可编程型。

ASIC(专业芯片)的特点

芯片制造商预先把编码系统用固化的专用硬件加以实现,保证上电即工作。开发用户只需对芯片作整体简单的初始化或对电阻电容的工作电压或电流的配置,输入视频,输出码流,因此编码芯片对开发者来说是黑盒
子。基于ASIC芯片的视频编解码系统的开发周期短,系统相对较稳定,易快速的批量生产。但是,系统不可升级,无论是功能增加升级或不稳定因素解除,其均无能为力。MPEG-4编码芯片有VW2010,H.264编码芯片有富士通的MB86H51,华为海思的Hi351x,美信MG2580/3500等。另外,芯片除了实现编解码外,还内嵌了ARM等处理器以侧重运行操作系统,如Hi351x、MG3500等芯片。

可编程性芯片特点

开发者利用高级或低级开发语言,根据视频算法功能开发出来的视频产品。通用的CPU(Intel/AMD)、GPU、DSP、FPGA等均为可编程芯片,跨平台的C语言或类似语言等基本都能基于这些芯片进行算法开发。
个人电脑PC的CPU功能强大,VC/VB等高级开发软件允许用户充分发挥个人的聪明才智,开发底层的数据处理算法或高层的人机用户应用程序。针对图像图形处理的GPU,强大的流水处理、单指令多数据SIMD操作、图像专用处理模块等极大增强了用户的图像开发能力。数字媒体的信号处理平台DSP以其独特的资源和功能配置结构,有针对性的数字媒体处理芯片等为开发者提供了便利而又强大的开发资源。数字媒体可编程处理器的最大特点是开发者能够掌握知识产权,易维护升级、功能定制。同时,独有的视频处理功能显著增加一般视频编码系统的附加值,形成自己特色的视频处理产品。

开源的视频编解码器(CODEC)

开发底层语言通常采用C语言编程,一般是基于视频编码协议,由专业的大神开发的快速且实用的算法工程。

ffdshow

一个支持多种音视频格式的基于DirectShow及VFW的CODEC集合,包
含:H.264、MPEG-4、MPEG-2、H.263、VP3、VP6、Theora、MJPEG、SVQ3、MP3、AC3、DTS、E-AC3、AAC和Vorbis。

ffmpeg

一个完整的跨平台音视频解决方案,能记录、转换和流处理音视频,
并包含领先的音视频CODEC库——libavcodec。

VLC

一个免费、开源的跨平台多媒体播放器和框架,能够播放大部分多媒体
文件,包括DVD、音频CD、VCD和各种流媒体。

Xvid

一个开源的MPEG-4视频编码和解码CODEC,实现了MPEG-4的
SP(Simple Profile)和ASP(Advanced Simple Profile)等档级。

T264

一个实现H.264视频编码和解码的开源算法,但是码流与H.264标准协议
略有不同,编程风格类似Xvid。

x264

一个免费的H.264视频编码开源算法,编码后的码流符合H.264/AVC格
式,很多H.264视频编码方案几乎都基于该蓝本。它与VLC同属于一个组织VideoLAN。

JM

JVT提供的H.264协议的参考校验模型,包括视频编码和解码两部分。目前,该模型的最新版本为JM17.2。MPEG-1和MPEG-2编解码源码从MPEG的主页上可直接获取。

小结

主流的视频编码算法基本是混合编解码技术,即包含预测、变换、量化和编码等步骤。帧间编码是视频编码压缩的主要方式,而帧间预测即运动估计算法是编码系统的核心。当下主流的视频编码算法主要有MPEG-4和H.264。个人觉得技术什么是很重要的,毕竟是程序员的饭碗,开发一个新技术或者在前人的基础上优化改进都一样有意义。博客内容均来自书籍,如有不足之处,敬请指出,我会积极采纳改进,我是Mr.小艾。

作者:qq_26986211 发表于2016/10/11 11:12:29 原文链接
阅读:22 评论:0 查看评论

CoreText(六):用户点击

$
0
0

1、添加手势

- (void)configSettings{
    //添加手势
    UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
    longPressGesture.minimumPressDuration = 0.01;
    longPressGesture.delegate = self;
    [self addGestureRecognizer:longPressGesture];
}

2、绘制

- (void)drawRect:(CGRect)rect{
    [super drawRect:rect];
    [self drawRectWithCheckClick];
}
#pragma mark - 绘制
- (void)drawRectWithCheckClick{

    // 1.创建需要绘制的文字
    NSMutableAttributedString *attributed = [[NSMutableAttributedString alloc] initWithString:self.text];

    // 2.1设置行距等样式
    [[self class] addGlobalAttributeWithContent:attributed font:self.font];

    // 2.2识别特定字符串并改变其颜色
    [self recognizeSpecialStringWithAttributed:attributed];

    //2.3加一个点击改变字符串颜色的效果
    if (self.pressRange.location != 0 && self.pressRange.length != 0){
        [attributed addAttribute:NSForegroundColorAttributeName value:[UIColor yellowColor] range:self.pressRange];
    }

    self.textHeight = [[self class] textHeightWithText:self.text width:CGRectGetWidth(self.bounds) font:self.font type:self.drawType];

    // 3.创建绘制区域,path的高度对绘制有直接影响,如果高度不够,则计算出来的CTLine的数量会少一行或者少多行
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, CGRectGetWidth(self.bounds), self.textHeight*2));

    // 4.根据NSAttributedString生成CTFramesetterRef
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributed);

    CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, NULL);
    self.ctFrame = CFRetain(ctFrame);

    // 获取上下文
    CGContextRef contextRef = UIGraphicsGetCurrentContext();

    // 转换坐标系
    CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
    CGContextTranslateCTM(contextRef, 0, self.textHeight); // 此处用计算出来的高度
    CGContextScaleCTM(contextRef, 1.0, -1.0);

    // 一行一行绘制
    CFArrayRef lines = CTFrameGetLines(ctFrame);
    CFIndex lineCount = CFArrayGetCount(lines);
    CGPoint lineOrigins[lineCount];

    // 把ctFrame里每一行的初始坐标写到数组里,注意CoreText的坐标是左下角为原点
    CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);

    CGFloat frameY = 0;
    for (CFIndex i = 0; i < lineCount; i++){
        // 遍历每一行CTLine
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);

        CGFloat lineAscent;
        CGFloat lineDescent;
        CGFloat lineLeading; // 行距
        // 该函数除了会设置好ascent,descent,leading之外,还会返回这行的宽度
        CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);

        CGPoint lineOrigin = lineOrigins[i];

        // 微调Y值,需要注意的是CoreText的Y值是在baseLine处,而不是下方的descent。
        CGFloat lineHeight = self.font.pointSize * kPerLineRatio;
        frameY = self.textHeight - (i + 1)*lineHeight - self.font.descender;
        lineOrigin.y = frameY;
        // 调整坐标
        CGContextSetTextPosition(contextRef, lineOrigin.x, lineOrigin.y);
        CTLineDraw(line, contextRef);
    }
    CFRelease(path);
    CFRelease(framesetter);
    CFRelease(ctFrame);
}

3、给字符串添加属性

#pragma mark - 工具方法
#pragma mark 给字符串添加全局属性,比如行距,字体大小,默认颜色
+ (void)addGlobalAttributeWithContent:(NSMutableAttributedString *)attributeString font:(UIFont *)aFont{
    CGFloat lineLeading = kGlobalLineLeading; // 行间距
    //设置部分颜色
    [attributeString addAttribute:(NSString *)kCTForegroundColorAttributeName value:(id)[UIColor greenColor].CGColor range:NSMakeRange(10, 10)];
    //设置文字
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", aFont.pointSize, NULL);
    [attributeString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:NSMakeRange(0, attributeString.length)];
    CFRelease(fontRef);
    // 设置行距等样式
    CGFloat lineSpace = lineLeading; // 行距一般取决于这个值
    CGFloat lineSpaceMax = 20;
    CGFloat lineSpaceMin = 2;
    const CFIndex kNumberOfSettings = 3;
    // 结构体数组
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpace},
        {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpaceMax},
        {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpaceMin}
    };
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);

    [attributeString addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge  id)theParagraphRef range:NSMakeRange(0, attributeString.length)];

    // 内存管理
    CFRelease(theParagraphRef);
}

4、识别相关字符串

#pragma mark - 识别特定字符串并改其颜色,返回识别到的字符串所在的range
- (NSMutableArray *)recognizeSpecialStringWithAttributed:(NSMutableAttributedString *)attributed{
    NSMutableArray *rangeArray = [NSMutableArray array];

    // 识别@人名
    NSRegularExpression *atRegular = [NSRegularExpression regularExpressionWithPattern:kAtRegularExpression options:NSRegularExpressionCaseInsensitive error:nil];
    NSArray *atResults = [atRegular matchesInString:self.text options:NSMatchingWithTransparentBounds range:NSMakeRange(0, self.text.length)];
    for (NSTextCheckingResult *checkResult in atResults){
        if (attributed){
            [attributed addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(checkResult.range.location, checkResult.range.length -1)];
        }
        [rangeArray addObject:[NSValue valueWithRange:checkResult.range]];
    }
    // 识别连续的数字
    NSRegularExpression *numberRegular = [NSRegularExpression regularExpressionWithPattern:kNumberRegularExpression options:NSRegularExpressionCaseInsensitive|NSRegularExpressionUseUnixLineSeparators error:nil];
    NSArray *numberResults = [numberRegular matchesInString:self.text options:NSMatchingWithTransparentBounds range:NSMakeRange(0, self.text.length)];
    for (NSTextCheckingResult *checkResult in numberResults){
        if (attributed){
            [attributed addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(checkResult.range.location, checkResult.range.length-1)];
        }
        [rangeArray addObject:[NSValue valueWithRange:NSMakeRange(checkResult.range.location, checkResult.range.length -1)]];
    }
    return rangeArray;
}

5、固定行高

/**
 *  固定行高
 *  高度 = 每行的固定高度 * 行数
 */
+ (CGFloat)textHeightWithText3:(NSString *)aText width:(CGFloat)aWidth font:(UIFont *)aFont{
    NSMutableAttributedString *content = [[NSMutableAttributedString alloc] initWithString:aText];
    // 给字符串设置字体行距等样式
    [self addGlobalAttributeWithContent:content font:aFont];
    CTFramesetterRef framesetterRef = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)content);
    // 粗略的高度,该高度不准,仅供参考
    CGSize suggestSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetterRef, CFRangeMake(0, content.length), NULL, CGSizeMake(aWidth, MAXFLOAT), NULL);

    CGMutablePathRef pathRef = CGPathCreateMutable();
    CGPathAddRect(pathRef, NULL, CGRectMake(0, 0, aWidth, suggestSize.height));

    CTFrameRef frameRef = CTFramesetterCreateFrame(framesetterRef, CFRangeMake(0, content.length), pathRef, NULL);

    CFArrayRef lines = CTFrameGetLines(frameRef);
    CFIndex lineCount = CFArrayGetCount(lines);

    // 总高度 = 行数*每行的高度,其中每行的高度为指定的值,不同字体大小不一样
    CGFloat accurateHeight = lineCount * (aFont.pointSize * kPerLineRatio);
    CGFloat height = accurateHeight;

    CFRelease(pathRef);
    CFRelease(frameRef);

    return height;
}

6、手势相关

#pragma mark - 手势识别相关
- (void)longPress:(UIGestureRecognizer *)gesture{
    // 改变字符串的颜色并进行重绘
    if (gesture.state == UIGestureRecognizerStateBegan){
        if (self.pressRange.length != 0||self.pressRange.location != 0) {
            [self setNeedsDisplay];
        }
    }else if(gesture.state == UIGestureRecognizerStateEnded){
        if (self.pressRange.location != 0 && self.pressRange.length != 0){
            NSString *clickStr = [self.text substringWithRange:self.pressRange];
            NSLog(@"点击了 %@",clickStr);
            self.pressRange = NSMakeRange(0, 0);
            [self setNeedsDisplay];
        }
    }
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    // 点击处在特定字符串内才进行识别
    BOOL gestureShouldBegin = NO;
    CGPoint location = [gestureRecognizer locationInView:self];
    //单行高度
    CGFloat lineHeight = self.font.pointSize * kPerLineRatio;
    //点击行数
    int lineIndex = location.y/lineHeight;
    // 把点击的坐标转换为CoreText坐标系下
    CGPoint clickPoint = CGPointMake(location.x, self.height - location.y);

    CFArrayRef lines = CTFrameGetLines(self.ctFrame);
    if (lineIndex < CFArrayGetCount(lines)){
        CTLineRef clickLine = CFArrayGetValueAtIndex(lines, lineIndex);
        // 点击处的字符位于总字符串的index
        CFIndex strIndex = CTLineGetStringIndexForPosition(clickLine, clickPoint);
        NSMutableAttributedString *mutableAttributed = [[NSMutableAttributedString alloc] initWithString:self.text];
        NSArray *checkResults = [self recognizeSpecialStringWithAttributed:mutableAttributed];
        for (NSValue *value in checkResults){
            NSRange range = [value rangeValue];
            if (strIndex >= range.location && strIndex <= range.location + range.length){
                self.pressRange = range;
                gestureShouldBegin = YES;
            }
        }
    }
    return gestureShouldBegin;
}
// 该方法可实现也可不实现,取决于应用场景
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
    {
        return YES; // 避免应用在UITableViewCell上时,挡住拖动tableView的手势
    }

    return NO;
}

这里写图片描述

作者:xiaoxiaobukuang 发表于2016/10/11 11:14:33 原文链接
阅读:22 评论:0 查看评论

Swift 结构体简单学习

$
0
0

这里写图片描述

定义

    /// 这里我定义了一个结构体类型
    /// 结构体类型是一个新的Siwft类型 (命名类似于 Int Double)
    /// 用来描述一个人的所带属性 name = "" age = 0
    struct PersonStruct {
        var name = " "
        var age = 0
    }
    /// 接下来我定义了一个类
    /// 其中包含PersonStruct结构体类型
    class Person{
        var personstruct = PersonStruct()
        var habbit = " "
    }

实例化

    /// 其中PersonStruct 和Person定义了抽象的PersonStruct, Person, 接下来我们进行实例化
    /// 结构体和类都使用构造器语法来生成新的实例, 在结构体名和类名后加()
    let personStruct = PersonStruct()
    let person = Person()

属性访问

    /// 属性访问
    print("The PersonStruct Name is \(personStruct.name)")
    /// 属性赋值操作
    /// 可以直接访问结构体的子属性
    person.personstruct.name = "summmerxx"
    person.habbit = "love coding"
    print("person->personstruct->name is \(person.personstruct.name) habbit is \(person.habbit)")

结构体赋值的例子

    let obj = PersonStruct(name: "summerxx2", age: 18)
    var obj1 = obj
    /// 把obj1.name 修改为暖暖
    obj1.name = "暖暖"
    /// 接下来我们来看看
    print("obj name is \(obj.name)") // summerxx2
    print("obj1 name is \(obj1.name)") // 暖暖
    /// 在将 obj 赋予给 obj1 的时候,实际上是将 obj 中所存储的值(values)进行拷贝. 只是单纯的进行值拷贝 

再看看类

    /// 类的操作
    let personObj = Person()
    personObj.habbit = "Beatiful things"

    let personObj1 = personObj
    personObj1.habbit = "Not habbit"

    print("personObj habbit is \(personObj.habbit)") // Not habbit
   /// 在将 personObj 赋予给 personObj1 的时候,实际上是将 personObj 存储的地址值进行拷贝.

接下来看看使用场景, 究竟什么情况下使用结构体, 在Objective-C开发的时候, 用于数据存储我一般都是使用类, 也就是MVC中的Model

  • 结构体的主要目的是用来封装少量相关简单数据值。
  • 有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。
  • 任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用
  • 结构体不需去继承另一个已存在类型的属性或者行为。

总结: Siwft 结构体简单学习就到这, 待补充~

作者:sinat_30162391 发表于2016/10/11 11:31:15 原文链接
阅读:20 评论:0 查看评论

AR增强现实开发介绍

$
0
0
  AR增强现实开发介绍

                                                                                ---理论篇

AR增强现实开发最近做一些AR增强现实的内容,一些普及性的内容,与大家分享。


一: 什么是AR增强现实技术:

      是一种将真实世界信息和虚拟世界信息“无缝”集成的新技术。是把原本在现实世界的一定时间空间范围内很难体验到的实体信息(视觉信息,声音,味道,触觉等),通过电脑等科学技术,模拟仿真后再叠加,将虚拟的信息应用到真实世界,被人类感官所感知,从而达到超越现实的感官体验。



二: AR增强现实技术突出的特点:

  1: ​真实世界和虚拟世界的信息集成
  2: 具有实时交互性
  3: 是在三维尺度空间中增添定位虚拟物体



三: VR与AR的区别
       虚拟现实(VR),看到的场景和人物全是假的,是把你的意识代入一个虚拟的世界。增强现实(AR),看到的场景和人物一部分是真一部分是假,是把虚拟的信息带入到现实世界中。


四: 适用范围
        AR技术不仅在与VR技术相类似的应用领域,诸如尖端武器、飞行器的研制与开发、数据模型的可视化、虚拟训练、娱乐与艺术等领域具有广泛的应用,而且由于其具有能够对真实环境进行增强显示输出的特性,在医疗研究与解剖训练、精密仪器制造和维修、军用飞机导航、工程设计和远程机器人控制等领域,具有比VR技术更加明显的优势。
        PS: 谷歌认为,增强现实才是未来的发展趋势,因为它能够带给人们更多互动体验,而非虚拟现实的隔离。



五: AR增强现实的应用领域:

1: 医疗领域:医生可以利用增强现实技术,轻易地进行手术部位的精确定位。

​2: 军事领域:部队可以利用增强现实技术,进行方位的识别,获得实时所在地点的地理数据等重要军事数据。
3: 古迹复原和数字化文化遗产保护:文化古迹的信息以增强现实的方式提供给参观者,用户不仅可以通过HMD看到古迹的文字解说,还能看到遗址上残缺部分的虚拟重构。

4: 工业维修领域:通过头盔式显示器将多种辅助信息显示给用户,包括虚拟仪表的面板、被维修设备的内部结构、被维修设备零件图等。

5: 电视转播领域:通过增强现实技术可以在转播体育比赛的时候实时的将辅助信息叠加到画面中,使得观众可以得到更多的信息。
6: 娱乐、社交、游戏领域:增强现实游戏可以让位于全球不同地点的玩家,共同进入一个真实的自然场景,以虚拟替身的形式,进行网络对战。

7: 旅游、展览领域:人们在浏览、参观的同时,通过增强现实技术将接收到途经建筑的相关资料,观看展品的相关数据资料。


8: 市政建设规划:采用增强现实技术将规划效果叠加真实场景中以直接获得规划的效果。
9: 益智类教育行业:


六: AR增强现实开发的开发厂家举例:

1:  HoloLens是微软公司开发的一款AR眼镜,于北京时间2015年1月22日凌晨发布,开发者版本售价为3000美元。 HoloLens的定位是使用户在产品的使用中拥有良好的交互体验。HoloLens拥有投射新闻信息流、模拟游戏、收看视频查看天气、辅助3D建模、协助模拟登陆火星场景等多方面的功能。


2: Google Glass
      2012年4月,Google发布了AR眼镜Google Project Glass,开发者版售价为1500美元。Google Glass可以通过语音指令,实现拍摄照片、发送信息、直播、辅助教学等功能。2015年1月19日,谷歌停止了谷歌眼镜的“探索者”项目。


3:亮风台(国内企业举例)
      成立于2012年11月,是一家专注于增强现实核心技术和产品研发的公司。在AR布局方面,亮风台的做法是从AR底层SDK到系统OS再到应用商店、浏览器、内容发布平台都将涉猎,最后通过终端贯穿整个行业。2015年11月,亮风台首次公布其AR眼镜HiAR Glasses。




下一篇: AR增强现实开发介绍: 开发基础篇
作者:liu_guozhu 发表于2016/10/14 11:10:55 原文链接
阅读:16 评论:0 查看评论

iOS静态库制作

$
0
0

QA

静态库与动态库的区别

  • 静态库:在编译的时候被完整地链接到可执行文件中,同一个静态库在不同程序中使用,每个程序都得导入一次,打包时也会被包进去,使其成为程序的一部分。
  • 动态库:程序运行的时候由系统动态地加载进内存,供程序调用,本身并不是程序的一部分。(iOS只支持系统的动态库)

静态库/动态库形式

  • 静态库: .a 和 .framework
  • 动态库: .dylib 和 .framework (.dylib现已被苹果替换成.tdb)

两种静态库形式的区别

  • .a静态库:单纯的编译之后的二进制格式文件,必须配合.h即相关头文件以及必要的资源文件才能才工程中使用。
  • .framework静态库:除了二进制文件,还有头文件和资源文件(.framework = .a + .h + Source File),严格来说.framework是一个静态库包,包含静态库文件。

.a静态库的制作

【第一步】创建工程,工程命名即为最终输出的静态库名字。

【第二步】创建或导入要打包进静态库的文件,将需要用到的资源文件,统一放到bundle包中(关于bundle包,下面有会有介绍)。由于新创建的工程还没有编译,Products下的.a文件还未生成,显示是红色的。

【第三步】选择要导出的.h文件

【第四步】选择编译模式(Debug、release)
在release模式下编译出的静态库相比Debug模式生成的静态库会去掉源码中用于调试的部分,即用Debug宏限定执行的部分,因此体积会略小,而且会进行一些优化。因此通常调试时使用Debug模式下输出的静态库,发布时使用release模式下输出的静态库(本例将以Debug模式演示)。

Procduct-->Scheme-->Edit Scheme Build Configuration下设置编译模式,默认为Debug模式。

【第五步】设置静态库支持的平台架构

目前模拟器和真机总共有5种架构

架构 平台 机型(iPhone) 描述
i386 模拟器 4S ~ 5 32位
x86_64 模拟器 5S ~ 现在的机型 64位
armv7 真机 3GS ~ 4S 32位
armv7s 真机 5 ~ 5C 特殊的架构
amr64 真机 5S ~ 现在的机型 64位

其中armv7s架构由于存在一定的问题,XCode6以后默认从标准架构列表中移除了,如果想要制作包含armv7s架构的静态库,可以在目标架构中添加armv7s。

但是由于armv7架构的静态库可以在armv7s设备上跑,因此通常不需要打armv7s的静态库,armv7和amr64就能够满足所有机型了。

要制作静态库,需要选择生成哪些架构的静态库。有两种选择:①生成当前平台对应架构的静态库 ②生成全平台架构的静态库。具体在 TARGETS-->Build Settings-->Build Active Architecture Only 中设置:

Debug项和release项分别针对Debug模式和release模式。值为YES表示只生成当前平台对应架构的静态库,值为NO表示生成全平台架构的静态库。例如,若在模拟器iPhone6s下编译,若当前模式下【Build Active Architecture Only】为 YES ,则生成的静态库只支持x86_64架构,若为 NO ,则生成的静态库包含模拟器所有平台的架构,即i386和x86_64。对于真机而言也是一样,若选择真机编译,【Build Active Architecture Only】同样决定了输出静态库所支持的架构。但是如果选择在【generic iOS Device】也就是【通用iOS设备】下编译,输出的静态库默认包含了全平台架构,也就是【Architectures】中指定的架构,默认为【standard architectures】,由于armv7s已经从标准架构列表中移除,因此最终输出包含armv7、amr64两种架构。

可以用下面命令来查看指定静态库所支持的架构。

lipo -info LIB.a

OK,为了输出支持全平台架构的静态库,确保将当前模式下【Build Active Architecture Only】设置为NO。

【第六步】输出静态库

当前目标是在Debug模式下输出模拟器和真机全平台架构的静态库。先输出真机架构的静态库,将目标运行设备选择为【Generic iOS Device】,然后【Command+B】,之后进入到Products目录(可以发现XCode工程中Products下的.a文件不再是红色的了,可以直接右键–show in finder进入到文件所在目录),会发现多了一个 Debug-iphoneos 文件,表明这是在Debug模式下编译的真机架构的静态库,里面是输出的.a文件和头文件,具体支持哪些架构可以通过lipo -info命令查看。

选择任意一种模拟器机型,同样【Command+B】编译,会在Products下输出一个Debug-iphonesimulator目录,里面就是模拟器架构的.a文件和头文件了。

【第七步】合并静态库
将真机架构的静态库和模拟器架构的静态库合并,使用命令

lipo -create 真机静态库.a 模拟器静态库.a -output 目标静态库.a

之前说了静态库要能使用需要配合.h文件以及资源文件(需要的话),但是资源包是不会自动输出的。手动输出后,最终得到这样一个完整的静态库文件

至此.a静态库已制作完毕,只要将整个文件add到工程里,然后引入头文件就能使用了,这里就不做演示了。

.framework静态库制作

【第一步】创建工程,工程名即为输出静态库名

【第二步】添加要打包的源码以及包装好资源文件的bundle包,默认生成的头文件若不用可以删掉

【第三步】选择要暴露的头文件

TARGETS-->Build Phases-->Headers 将project列表中要暴露的头文件移动到Public目录下。

【第四步】选择编译模式(Debug/release),同.a静态库制作,不再赘述
【第五步】设置输出静态库架构,同.a静态库制作,不再赘述
【第六步】设置Deployment Target

【第七步】将输出设置为静态库(默认为动态库)

TARGETS-->Build Settings-->Linking-->Mach-O Type 设置为 Static Library

【第八步】输出静态库。分别在真机和模拟器下编译工程,方法同.a静态库的制作过程。最终在Products目录下输出两个.framework文件,分别对应真机和模拟器架构,均为Debug模式下编译的。可以看到,头文件以及bundle包被一并输出到静态库中了。

【第九步】合并静态库,将真机架构的静态库与模拟器架构的静态库合并,需要注意的是合并的是两个.framework包内名为LIB的文件,这才是真正的静态库。

最后将合并出的静态库文件替换掉任意一个.framework包内的静态库文件,得到的.framework就是一个完整的静态库包。

使用时同样只要将.framework包add到工程中,源码中引入相应的头文件即可,至此.framework静态库制作完毕。

Tip

资源文件的处理

两种静态库,一般都是把资源文件单独的放在一个.bundle文件中,一般.bundle的名字和.a或.framework的名字相同。.bundle文件很好弄,新建一个文件夹,把后缀改名为.bundle就可以了,右键,显示包内容可以向其中添加资源。

Bundle包内资源路径

静态库里如果用到bundle包里的资源文件,需要注意资源路径。因为整个bundle包会被原封不动地打到工程的main bundle里去,因此直接以资源名是获取不到资源的。以图片为例,一种方法是使用的时候写全资源路径:

    UIImage *image = [UIImage imageNamed:@"LIB.bundle/image"];

另一种方法是先获取到bundle的路径,然后获取资源:

    NSString *path = [[NSBundle mainBundle] pathForResource:@"LIB" ofType:@"bundle"];
    NSBundle *LIBBundle = [NSBundle bundleWithPath:path];
    NSString *imagePath = [LIBBundle pathForResource:@"image" ofType:@"png"];
    UIImage *image = [[UIImage alloc]initWithContentsOfFile:imagePath];

另外对bundle中的图片,系统会自动适配,只要命名好该有的2x 3x就行了!不信的话在不同retina下把图片scale打出来看看!

含分类的静态库

分类(Category)是我们实际开发项目中经常用到的,把Category打成静态库是没有问题的,但是在使用这个静态库的工程中,调用category中的方法时会有找不到该方法的运行时错误“selector not recognized”,原因是Unix的标准静态库实现和OC的动态特性之间有一些冲突:OC没有为每个函数(或者方法)定义链接符号,它只为每个类创建链接符号。这样当在一个静态库中使用类别来扩展已有类的时候,链接器不知道如何把类原有的方法和类别中的方法整合起来,就会导致你调用类别中的方法时,出现”selector not recognized”,也就是找不到方法定义的错误。解决这个问题的方法是在使用静态库的工程中配置other linker flags的值为-ObjC,它的作用就是将静态库中所有的和对象相关的文件都加载进来。

然而如果类库中只有category而没有类的时候这些category还是加载不进来,方法就是加入-all_load或者-force-load标记。-all_load会强制链接器把目标文件都加载进来,即使没有objc代码。-force_load在xcode3.2后可用,但是-force_load后面必须要指定具体的文件。

作者:Lotheve 发表于2016/10/14 11:17:26 原文链接
阅读:31 评论:0 查看评论

iOS开发 APP如何实现检测更新

$
0
0

App检测更新可以使用两种方法。

第一种是和安卓等系统一样,获取自己服务器的App版本号与已安装的App版本号比较;

第二种是根据已发布到App Store上的应用版本号与已安装的App版本号比较更新。

两种方法比较

第一种检测更新方法的优点是:检测更新速度快、检测稳定;缺点是:和App Store上的应用版本号不同步(App上架需要审核时间,不确定什么时候成功更新到App Store上)。

第二种方法检测更新方法的优点是:检测版本号是实时同步的;缺点是:苹果网络不稳定,检测更新有点延时,部分App获取不到任何参数。代码在github的cjq002的CheckVersion上。

大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑大笑

版本号比较方法

      1、获取App当前版本号;

      2、使用NSString自带方法进行比较。


跳转到App Store下载

      1、格式化下载链接;

      2、使用系统自带方法跳转到App Store应用下载页。


方法一:获取自己服务器版本号检查

      1、通过网络请求获取服务器上的版本号;

      2、调用上面的比较方法,比较前应用版本号和服务器上的版本号;

      3、如果有版本更新则跳转到App Store上下载。

注:获取服务器版本号就需要自己去请求了。

方法二:获取App Store上架版本号检查

      1、通过网络同步请求获取App Store上对应APP ID的应用信息;

      2、提取信息上的最新版本号等信息;

      3、提取最新版本号;

      4、调用上面的比较方法,比较前应用版本号和最新版本号;

      5、如果有版本更新则跳转到App Store上下载。



运行效果(以第二种方法,iOS版企鹅应用为例)

      当前版本为3.2.1,请求控制台返回:“发现新版本 6.5.6”

(Demo在真机上会跳转到AppStore的企鹅下载页);

      当前版本为6.5.6,请求控制台返回:“没有新版本”;

      当前版本为6.6.6,请求控制台返回:“没有新版本”。




以上是全部步骤, 为了方便大家使用,下面粘上代码。

- (BOOL)compareVersion:(NSString *)serverVersion {

    // 获取当前版本号

    NSDictionary *infoDic = [[NSBundle mainBundle] infoDictionary];

    NSString *appVersion = [infoDic objectForKey:@"CFBundleShortVersionString"];

    

    // MARK: 比较当前版本和新版本号大小

    /*

     typedef enum _NSComparisonResult {

        NSOrderedAscending = -1L,   升序

        NSOrderedSame,              等于

        NSOrderedDescending         降序

     }

     */

    

    // MARK 比较方法

    if ([appVersion compare:serverVersion options:NSNumericSearch] == NSOrderedAscending) {

        NSLog(@"发现新版本 %@", serverVersion);

        return YES;

    }else {

        NSLog(@"没有新版本");

        return NO;

    }

    

}


- (void)aaa {

    

    // 下载地址可以是trackViewUrl, 也可以是items-apps://itunes.apple.com/app/idxxxxxxxxxx

    NSString *appId = @"xxxxxxxxx";

    NSString *string = [NSString stringWithFormat:@"items-apps://itunes.apple.com/app/id%@", appId];

    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:string]];

}


- (BOOL)checkAppStoreVersionWithAppId:(NSString *)appId {

    

    // MARK: 拼接链接,转换成URL

    NSString *checkUrlString = [NSString stringWithFormat:@"https://itunes.apple.com/lookup?id=%@", appId];

    NSURL *checkUrl = [NSURL URLWithString:checkUrlString];

    

    // MARK: 获取网路数据AppStoreapp的信息

    NSString *appInfoString = [NSString stringWithContentsOfURL:checkUrl encoding:NSUTF8StringEncoding error:nil];

    

    // MARK: 字符串转json转字典

    NSError *error = nil;

    NSData *JSONData = [appInfoString dataUsingEncoding:NSUTF8StringEncoding];

    NSDictionary *appInfo = [NSJSONSerialization JSONObjectWithData:JSONData options:NSJSONReadingMutableLeaves error:&error];

    

    if (!error && appInfo) {

        NSArray *resultArr = appInfo[@"results"];

        NSDictionary *resultDic = resultArr.firstObject;

        

        // 版本号

        NSString *version = resultDic[@"trackName"];

        

        // 下载地址

        NSString *trackViewUrl = resultDic[@"trackViewUrl"];

        

        // FRXME:比较版本号

        return [self compareVersion:version];

        

    }else {

        // 返回错误 想当于没有更新吧

        return NO;

    }

    

}


- (void)viewDidLoad {

    

    [super viewDidLoad];

    

    static NSString *appId = @"xxxxxx";

    

    // 返回是否有新版本

    BOOL update = [self checkAppStoreVersionWithAppId:appId];

    

    // 添加自己的代码 可以弹出一个提示框 这里不实现了

    if (update) {

        // 下载地址可以是trackViewUrl, 也可以是item-apps://itunes.apple.com/app/idxxxxxxxx

        NSString *string = [NSString stringWithFormat:@"items-apps://itunes.apple.com/app/idxxxxx"];

        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:string]];

    }

}



作者:CXLiao 发表于2016/10/14 11:17:49 原文链接
阅读:31 评论:0 查看评论

Android 开发规范

$
0
0

编程思想规范

1)对于字符串的匹配,使用最细匹配规则,
   如:对医生详情连接的匹配使用url.startsWith("http://m.1ping.com/Doctor/Detail/doctorDetail?did=")代替url.startsWith("http://m.1ping.com")

2)对数组的操作,需要做数组下表是否会越界的判断处理,以出现数组越界异常。

代码书写规范

1)方法之间的间距为三个回车

命名规范

类文件命名规范

 满足大驼峰规则:
1)每个单词首字母大写。如MyStudentCount;
2)且为名词;
3)对于工具类:使用Utils结尾

方法命名规范

 满足小驼峰规则:
1)除第一个单词之外,每个单词首字母大写。如getColor;
2)且为动词;

Id命名规范

1:对应view的前缀
 控件              Id命名缩写前缀 
 botton               btn_
 imageView            img_
 editText             edit_
 TextView             txt_
 linearLayout         llay_
 realtiveLayout       rlay_
 gridView             grid_
 ListView             lv_
 WebView              webv_
 ViewFlipper          vflp_
 ImageButton          imgb_
 RadioButton          rbtn_
 CheckBox             cbox_
 ProgressBar          prgb_
 AutoCompleteTextView actv_
 GridLayout           glay_
 FrameLayout          flay_
 TableLayout          tlay_
 ScrollView           scrlv_
 Gallery              glry_
 TimePicker           tmpic_
DatePicker           dtPk_
SeekBar              skBar_
ZoomControls         zmCtl_
VideoView            vdeov_
RantingBar           ratbar_
Tab                  tab_
Spinner              spn_
MapView              mapv_
TextSwitch           txtwwt_
ImageSwitch          imgswt_
Viewpager            vp_

代码中变量命名

1:常量:全部大写,采用下划线命名法.例如:MIN_WIDTH
2:静态变量:全部大写,多个单词则以_分开,比如BOOLEAN_FLAG
3:跟控件相关的变量:添加前缀,并满足小驼峰规则:edtvPhone
4:普通变量,满足小驼峰规则
5:Map对应的变量添加map前缀,HashMap添加hmap前缀

注:小驼峰规则:除第一个单词之外,其他单词首字母大写。如myStudentCount;

图片命名规范

遵从:功能从最大到最小,最范到最具体
大范围_中范围(...)_小范围
如 bg_banner_weather
bg_banner_message
前缀缩写:
bg:背景
icon:小图标

某些功能命名规范

功能 变量命名 id命名 内容命名 点击事件命名
用户名 username   userName  
昵称 nickname **_nickname nickName  
头像 avatar **_avatar   **Avatar
格言 motto **_motto motto  
find     find
二维码 ecode **_ecode ecode  
返回键 back      btn_back   goBack
左侧按键 btnLeft     btn_left    
电话号码框 edtvPhone   edtv_phone phone  
密码框 edtvPwd edtv_pwd pwd  
密码确认 edtvPwdSure edtv_pwd_sure pwdSure  
验证码框 edtvVcode edtv_vcode vcode getVcode
分享 **Share **_share share goShare
点赞 **Support **_support support goSupport
评论 **Comment **_comment comment goComment
收藏 **Collect **_collect collection addCollect
经度 longitude      
纬度 latitude      
object obj      
关闭 close      
打开 open      
向服务器请求 request**     request
获取普通数据 get     get
我的 mine      
医生 doctor      

资源文件夹中资源命名规范

规定:
  1)资源文件全为小写
2)相应文件前添加相应的前缀
3)在前缀后添加相应的功能
  4)用_分隔多个功能,功能范围从大到小

1:对于全局使用(唯一性)的资源命名:line_blue

2:按钮点击事件选择器:selector_
   用下划线分割功能,前面的颜色值为默认颜色,后面的颜色值为按下时的颜色。
   如:selector_white_black
       selector_round_rect_white_black
3: 形状文件:(round/rect/round_rect)_
4: 图片文件:(icon/bg)_
5: 动画文件
淡入     fade_in
淡出     fade_out
从下方推入   push_down_in
从下方推出   push_down_out
推像左方     push_left
从头部滑动进入  slide_in_from_top
变形进入       zoom_enter
滑动进入       slide_in
中间缩小      shrink_to_middle

6: 布局文件:
Activity对应视图: activity_
fragment对应视图: fragment_
include加载的视图:layout_
其他控件对应视图:  layout_
分割线视图:       line_

功能比较具体的视图可添加功能缩写为前缀:如对话框:dialog_

7:color,dimens资源文件命名规范

对于主题颜色:统一前缀 theme_color_

            后加颜色英文单词 gray_

            区分同一颜色值的英文字母后缀(注英文字母abc不代表等级) a

如:theme_color_blue_a   theme_color_blue_b

对于margin,padding,textSize等dimens值:统一前缀 theme_margin_   theme_padding_   theme_text_size_

                                      后加对应数值的英文单词作为后缀 five   ten   fourteen

如:theme_margin_five   theme_padding_ten   theme_text_size_fourteen

注释规范

1:类开头注释:
/**
*需要别的地方可以看到
*/

2:方法前注释:
/**
*需要别的地方可以看到
*/

3:块注释:
/*
*因为别的地方不会看到
*/

4:执行步骤或者变量注释:
// 并将此注释添加在备注释部分的后端,而不是上端     
如:private int name; //名字

5:静态变量使用:
/*** 这个注释是这个意思  */  放在变量上方

6:一段代码逻辑的注释,保持一行
/*这段是干嘛用的**/

7:添加TODO规范
必要的地方需要添加TODO,不需要的TODO及时删除。

文档结构规范

model文件

单model

复合model

 1. 为了防止空指针问题,需要在属性的get方法做判断
public Today getToday() { 
if(today==null){
today = new Today(); 
} 
return today; 
}

view文件

viewmodel文件

资源文件

drawable文件
全局性定义: a_控件_特性,如a_tab_selector_checked_bg.xml  表示tab控件中,选择的背景定义
view持有定义: 所属布局名_控件_特性  如 activity_main_tab_health.xml 表示activity_main中tab类型的控件健康档案定义

Log日志规范

1)info级别:
URL路径

请求参数

请求返回数据

跟服务器打交道的数据,都在这个类

2)error级别:
这个一般是系统输出的崩溃日志。

3)debug级别:
一般日志,就是我们平常需要跟踪一些调用顺序,一般都在这个类别加。最常用的就是这个。

4)warn级别:
一般try catch 的exception就是这个类别输出日志。

5)将Tag存放到数据库

Android Studio上发布版本规范

发布安装包命名规范

1、ypxs_版本名称_渠道号.apk

GIT操作规范

1、分支命名规定:
  pack_44:分支为用于打包的代码  pack代表打包,44代表开发版本号
  dev_44_im:分支为用于开发版本为44,在im部分进行功能修改

2、使用主干master开发

发布版本步骤:

1、检查App工程目录下的build.gradle文件中的 版本号版本名称 是否正确。

2、检查App工程目录下的build.gradle文件中的buildTypes的release是否配置为正式服务器,及正确添加混淆:如下代码显示

buildTypes {
release {
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.config
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

//正式发布的配置,注意:星号只是保密的一种代写
manifestPlaceholders.put("BaiduMobAd_KEY",'8c****f086')
manifestPlaceholders.put("UMENG_KEY",'556592a1******0044e9')
buildConfigField 'String','Base_Api_URL','"http://api.1ping.com"'

//渠道包重命名
applicationVariants.all { variant ->
variant.outputs.each { output ->
 def outputFile = output.outputFile
 if (outputFile != null && outputFile.name.endsWith('.apk')) {
     // 输出apk名称为boohee_v1.0_2015-01-15_wandoujia.apk
     def fileName = "ypxs_${defaultConfig.versionName}_${variant.productFlavors[0].name}.apk"
     output.outputFile = new File(outputFile.parent, fileName)
  }
}
}
}
}

3、检查上述文件之后,目前需要到开发代码额启动页代码中查看是否为首发状态。

首发状态:启动页图片为静态的并且为首发图片,不加载多张动态图片,所以动态加载并缓存三张启动页的代码需要注释掉

非首发状态:启动页基本页为医评原生启动静态页,并且将代码中加载三张启动页的部分开放出来。

4、上述检查完了,及可以打开Android_studio右边竖排上的Gradle工具,选择app》build,在build下       

   assembleRelease:双击即可进行多渠道的打包

assemble+渠道号(如assembelAzsc001):双击即进行单渠道打包

5、在打包结束后,可以在工程目录下的app》build》outputs》apk文件夹下看到我们需要的安装包。

6、
1)将打包好的版本交由测试进行测试,并告知更新的内容
2)测试将安装包测试,当确定无bug的情况下将新版本交给运维

文本输入框规范

控件属性

1.手机号码、身高等数值类型

android:inputType="number"

2.密码类型

android:inputType="textPassword"

正则表达式

1.验证邮箱的格式:\\w+@\\w+\\.[a-z]+(\\.[a-z]+)?

2.验证身份证格式:

15位身份证:^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$

18位身份证:^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$

3.验证手机号格式:(\\+\\d+)?1[34578]\\d{9}$

4.验证中文字符格式:^[\u4E00-\u9FA5]+$

国际化多语言说明

1.Android字符串国际化

只要在 res 文件夹下新建对应语言的 values 文件夹就好了

如,英语环境下的,文件夹命名为:values-en 

中文环境为:values-zh

当某一个资源没有在语言环境的对应的资源集合中找到时,就会使用 values 下的资源。 
若某一个语言环境没有在项目中定义语言环境,那么也会使用 values 下的资源。 

2.图片国际化

同理。 在 res 下新建 drawable-zh 文件夹,存放中文环境下的图片

新建 drawable-en 作为英语环境下的图片


Android多国语言文件夹文件汇总如下:

中文(中国):values-zh-rCN
中文(台湾):values-zh-rTW
中文(香港):values-zh-rHK
英语(美国):values-en-rUS
英语(英国):values-en-rGB
英文(澳大利亚):values-en-rAU
英文(加拿大):values-en-rCA
英文(爱尔兰):values-en-rIE

英文(印度):values-en-rIN
英文(新西兰):values-en-rNZ
英文(新加坡):values-en-rSG
英文(南非):values-en-rZA

阿拉伯文(埃及):values-ar-rEG
阿拉伯文(以色列):values-ar-rIL
保加利亚文:  values-bg-rBG
加泰罗尼亚文:values-ca-rES
捷克文:values-cs-rCZ
丹麦文:values-da-rDK
德文(奥地利):values-de-rAT
德文(瑞士):values-de-rCH
德文(德国):values-de-rDE
德文(列支敦士登):values-de-rLI
希腊文:values-el-rGR
西班牙文(西班牙):values-es-rES
西班牙文(美国):values-es-rUS
芬兰文(芬兰):values-fi-rFI
法文(比利时):values-fr-rBE
法文(加拿大):values-fr-rCA
法文(瑞士):values-fr-rCH
法文(法国):values-fr-rFR
希伯来文:values-iw-rIL
印地文:values-hi-rIN
克罗里亚文:values-hr-rHR
匈牙利文:values-hu-rHU
印度尼西亚文:values-in-rID
意大利文(瑞士):values-it-rCH
意大利文(意大利):values-it-rIT
日文:values-ja-rJP
韩文:values-ko-rKR
立陶宛文:valueslt-rLT
拉脱维亚文:values-lv-rLV
挪威博克马尔文:values-nb-rNO
荷兰文(比利时):values-nl-BE
荷兰文(荷兰):values-nl-rNL
波兰文:values-pl-rPL
葡萄牙文(巴西):values-pt-rBR
葡萄牙文(葡萄牙):values-pt-rPT
罗马尼亚文:values-ro-rRO
俄文:values-ru-rRU
斯洛伐克文:values-sk-rSK
斯洛文尼亚文:values-sl-rSI
塞尔维亚文:values-sr-rRS
瑞典文:values-sv-rSE
泰文:values-th-rTH
塔加洛语:values-tl-rPH
土耳其文:values--r-rTR
乌克兰文:values-uk-rUA
越南文:values-vi-rVN

多语言文件归类规范

1.strings_com:放置有关全局、搜索、设置、dialog提示语等string文字。

2.strings_doc:放置医生、医院、科室、会诊、资质认证、排行榜等与医生有关的string文字。

3.strings_im:消息模块。

4.strings_news:有关资讯、微课堂的。

5.strings_user:登录、注册、会员信息、健康档案、任务、订单等string文字。

作者:hai1059876295 发表于2016/10/14 11:19:04 原文链接
阅读:35 评论:0 查看评论

iOS用WKWebView与JS交互获取系统图片及WKWebView的Alert,Confirm,TextInput的监听代理方法使用,屏蔽WebView的可选菜单

$
0
0

最近做一个项目,开始是使用WebView与JS交互的,由于内存管理方面WebView欠佳。WKWebVIew的内存线程管理好,所以选择使用 WKWebVIew(使用WKWebView 的缺点在于,这个控件加载的H5页面不支持ajax请求,所以需要自己把网络请求在OC上实现)。

一、首先说下应该注意的问题:

1.要获取拍照或相册的图片,如果是iOS 10系统,需要设置访问权限(在 Info-plist 中设置)

相机权限: Privacy - Camera Usage Description 是否允许此App使用你的相机?
相册权限: Privacy - Photo Library Usage Description 是否允许此App访问你的媒体资料库?

2.WebView和WKWebView和JS互调的方法和使用的传参类型不同

WebView 使用 (window.iosModel.getImage(JSON.stringify(parameter)); //JSON.stringify(参数字符串) 这个方法是 把字符串转换成json字符串 parameter是参数字符串
   )传值给 OC

WKWebView 使用 (window.webkit.messageHandlers.iosModel.postMessage(parameter))

3.需要特别注意的是:WKWebView 不执行JS写的 ajax请求(WKWebView 可能由于基于 WebKit,并不会执行 C socket 相关的函数对 HTTP 请求进行处理)如果有网络请求,需要自己用OC实现

4.使用的时候不要忘记挂上使用到的代理,和导入代理

@interface ViewController () <WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate,UINavigationControllerDelegate,UIImagePickerControllerDelegate>

5.屏蔽WebView的可选菜单(即不会出现拷贝、全选等弹出菜单)在加载完成后的代理中执行以下两段JS

// 导航完成时,会回调(也就是页面载入完成了)
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
    NSLog(@"66===%s", __FUNCTION__);
    // 禁用选中效果
    [self.webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none'" completionHandler:nil];
    [self.webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none'" completionHandler:nil];
}




二、代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
		<div>
            <h3>JS与iOS交互</h3>
            <h4>JS页面获取iOS系统图片</h5>
        </div>
		<div>
			<input type = "button" style="width: 50%;height: 5%;" id="Button" value="打开相机获取图片" onclick="getIOSImage()"></button>
		</div><dir />
		<div>
			<img src="testImage.png" id="changeImage"style="width: 30%; height: 30%;" onclick="getIOSImage()"><!--src="图片的相对路径" 如果把html文件导入工程中,图片路径和OC一样只写图片名字和后缀就可以,(记得要先把图片添加到工程) 图片也可以实现按钮的方法getIOSImage -->
		</div>
        <span id="iosParame" style="width: 200px; height: 50%; color:orangered; font-size:15px" value="等待获取ios参数" >
            </div>
		<script>
			var getIOSImage = function(){
				var parameter = {'title':'JS调OC','describe':'这里就是JS传给OC的参数'};
				// 在下面这里实现js 调用系统原生api iosDelegate
                //JSON.stringify(参数字符串) 这个方法是 把字符串转换成json字符串
	window.iosDelegate.getImage(JSON.stringify(parameter));// 实现数据的 json 格式字符串
			}
        // 这里是 iOS调用js的方法
        function setImageWithPath(arguments){
            document.getElementById('changeImage').src = arguments['imagePath'];
            document.getElementById('iosParame').innerHTML = arguments['iosContent'];
        }
		</script>
	</body>
</html>

<pre name="code" class="objc">#import "ViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import <WebKit/WebKit.h>
#import "SaveImage_Util.h"

@interface ViewController () <WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate,UINavigationControllerDelegate,UIImagePickerControllerDelegate>

@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic, strong) UIProgressView *progressView;

@end

@implementation ViewController
{
    int indextNumb;// 交替图片名字
    UIImage *getImage;//获取的图片
}
- (void)viewDidLoad {
  [super viewDidLoad];
  
  self.edgesForExtendedLayout = UIRectEdgeNone;
  self.automaticallyAdjustsScrollViewInsets = NO;
  
  WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
  // 设置偏好设置
  config.preferences = [[WKPreferences alloc] init];
  // 默认为0
  config.preferences.minimumFontSize = 10;
  // 默认认为YES
  config.preferences.javaScriptEnabled = YES;
  // 在iOS上默认为NO,表示不能自动通过窗口打开
  config.preferences.javaScriptCanOpenWindowsAutomatically = NO;


  // web内容处理池
  config.processPool = [[WKProcessPool alloc] init];

  // 通过JS与webview内容交互
  config.userContentController = [[WKUserContentController alloc] init];
  // 注入JS对象名称AppModel,当JS通过AppModel来调用时,
  // 我们可以在WKScriptMessageHandler代理中接收到
  [config.userContentController addScriptMessageHandler:self name:@"iosModel"];



        //通过默认的构造器来创建对象
  self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    // 导航代理
    self.webView.navigationDelegate = self;
    // 与webview UI交互代理
    self.webView.UIDelegate = self;
  NSURL *path = [[NSBundle mainBundle] URLForResource:@"testJS" withExtension:@"html"];
  [self.webView loadRequest:[NSURLRequest requestWithURL:path]];
  [self.view addSubview:self.webView];
  
  

  // 添加KVO监听
  [self.webView addObserver:self
                 forKeyPath:@"loading"
                    options:NSKeyValueObservingOptionNew
                    context:nil];
  [self.webView addObserver:self
                 forKeyPath:@"title"
                    options:NSKeyValueObservingOptionNew
                    context:nil];
  [self.webView addObserver:self
                 forKeyPath:@"estimatedProgress"
                    options:NSKeyValueObservingOptionNew
                    context:nil];

  // 添加进入条
  self.progressView = [[UIProgressView alloc] init];
  self.progressView.frame = self.view.bounds;
  [self.view addSubview:self.progressView];
  self.progressView.backgroundColor = [UIColor blackColor];
  
  self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"后退" style:UIBarButtonItemStyleDone target:self action:@selector(goback)];
  self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"前进" style:UIBarButtonItemStyleDone target:self action:@selector(gofarward)];
}

- (void)goback {
  if ([self.webView canGoBack]) {
    [self.webView goBack];
  }
}

- (void)gofarward {
  if ([self.webView canGoForward]) {
    [self.webView goForward];
  }
}

#pragma mark - WKScriptMessageHandler
// 通过这个方法获取 JS传来的json字符串
- (void)userContentController:(WKUserContentController *)userContentController
      didReceiveScriptMessage:(WKScriptMessage *)message
{
  if ([message.name isEqualToString:@"iosModel"]) {
    // 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray,
    // NSDictionary, and NSNull类型
      NSLog(@"JS传来的json字符串 :  %@", message.body);
      NSDictionary *jsDictionary = message.body;
      if ([jsDictionary[@"means"] isEqualToString:@"获取系统图片"])
      {
          [self beginOpenPhoto];
      }
  }
}

#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
  if ([keyPath isEqualToString:@"loading"]) {
    NSLog(@"loading");
  } else if ([keyPath isEqualToString:@"title"]) {
    self.title = self.webView.title;
  } else if ([keyPath isEqualToString:@"estimatedProgress"]) {
    NSLog(@"progress: %f", self.webView.estimatedProgress);
    self.progressView.progress = self.webView.estimatedProgress;
  }
  if (!self.webView.loading) {
    [UIView animateWithDuration:0.5 animations:^{
      self.progressView.alpha = 0;
    }];
  }
}

#pragma mark - WKNavigationDelegate
    // 请求开始前,会先调用此代理方法
    // 与UIWebView的
    // - (BOOL)webView:(UIWebView *)webView
    // shouldStartLoadWithRequest:(NSURLRequest *)request
    // navigationType:(UIWebViewNavigationType)navigationType;
    // 类型,在请求先判断能不能跳转(请求)

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:
(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
  NSString *hostname = navigationAction.request.URL.host.lowercaseString;
  if (navigationAction.navigationType == WKNavigationTypeLinkActivated
      && ![hostname containsString:@".lanou.com"]) {
// 对于跨域,需要手动跳转, 用系统浏览器(Safari)打开
    [[UIApplication sharedApplication] openURL:navigationAction.request.URL];
    
    // 不允许web内跳转
    decisionHandler(WKNavigationActionPolicyCancel);
  } else {
    self.progressView.alpha = 1.0;
    decisionHandler(WKNavigationActionPolicyAllow);
  }
  
}


    // 在响应完成时,会回调此方法
    // 如果设置为不允许响应,web内容就不会传过来
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
  decisionHandler(WKNavigationResponsePolicyAllow);
  
}


// 开始导航跳转时会回调
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
   
}

    // 接收到重定向时会回调
- (void)webView:(WKWebView *)webView
didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
   
}


    // 导航失败时会回调
- (void)webView:(WKWebView *)webView
didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
    
}


// 页面内容到达main frame时回调
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation {
    
}

// 导航完成时,会回调(也就是页面载入完成了)
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
    NSLog(@"66===%s", __FUNCTION__);
    // 禁用选中效果
    [self.webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none'" completionHandler:nil];
    [self.webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none'" completionHandler:nil];
}
    // 导航失败时会回调
- (void)webView:(WKWebView *)webView didFailNavigation:
(null_unspecified WKNavigation *)navigation withError:(NSError *)error
{
    
}


/* 对于HTTPS的都会触发此代理,如果不要求验证,传默认就行
    如果需要证书验证,与使用AFN进行HTTPS证书验证是一样的 */

- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:
(NSURLAuthenticationChallenge *)challenge completionHandler:
(void (^)(NSURLSessionAuthChallengeDisposition disposition,
          NSURLCredential *__nullable credential))completionHandler
{
  completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}


// 9.0才能使用,web内容处理中断时会触发
/*
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
}
*/
#pragma mark - WKUIDelegate
- (void)webViewDidClose:(WKWebView *)webView {
    
}
/* 在JS端调用alert函数时,会触发此代理方法。JS端调用alert时所传的数据可以通过message拿到 在原生得到结果后,需要回调JS,是通过completionHandler回调 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message
initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
  UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"alert" message:message preferredStyle:UIAlertControllerStyleAlert];
  [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    completionHandler();
  }]];
  
  [self presentViewController:alert animated:YES completion:NULL];
  NSLog(@"%@", message);
 
}

    // JS端调用confirm函数时,会触发此方法
    // 通过message可以拿到JS端所传的数据
    // 在iOS端显示原生alert得到YES/NO后
    // 通过completionHandler回调给JS端
- (void)webView:(WKWebView *)webView
runJavaScriptConfirmPanelWithMessage:(NSString *)message
initiatedByFrame:(WKFrameInfo *)frame
completionHandler:(void (^)(BOOL result))completionHandler {
  UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"confirm" message:message preferredStyle:UIAlertControllerStyleAlert];
  [alert addAction:[UIAlertAction actionWithTitle:@"确定"
    style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action){
    completionHandler(YES);
  }]];
  [alert addAction:[UIAlertAction actionWithTitle:@"取消"
  style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action)
    {
    completionHandler(NO);
  }]];
  [self presentViewController:alert animated:YES completion:NULL];
  NSLog(@"%@", message);
}
 // JS端调用prompt函数时,会触发此方法
    // 要求输入一段文本
    // 在原生输入得到文本内容后,通过completionHandler回调给JS
- (void)webView:(WKWebView *)webView
runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
    defaultText:(nullable NSString *)defaultText
initiatedByFrame:(WKFrameInfo *)frame
completionHandler:(void (^)(NSString * __nullable result))completionHandler
{
  UIAlertController *alert = [UIAlertController alertControllerWithTitle:prompt message:defaultText preferredStyle:UIAlertControllerStyleAlert];
  [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
    textField.textColor = [UIColor redColor];
  }];
  [alert addAction:[UIAlertAction actionWithTitle:@"确定"
    style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    completionHandler([[alert.textFields lastObject] text]);
  }]];
  
  [self presentViewController:alert animated:YES completion:NULL];
}

//  获取图片
- (void)beginOpenPhoto
{
    // 主队列 异步打开相机
    dispatch_async(dispatch_get_main_queue(), ^{
        [self takePhoto];
    });
}
#pragma mark 取消选择照片代理方法
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
    [picker dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark      //打开本地照片
- (void) localPhoto
{
    UIImagePickerController *imagePicker = [[UIImagePickerController alloc]init];
    imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    imagePicker.delegate = self;
    [self presentViewController:imagePicker animated:YES completion:nil];
}
#pragma mark      //打开相机拍照
- (void) takePhoto
{
    UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera;
    if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
    {
        UIImagePickerController *picker = [[UIImagePickerController alloc]init];
        picker.delegate = self;
        picker.allowsEditing = YES;
        picker.sourceType = sourceType;
        picker.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
        [self presentViewController:picker animated:YES completion:nil];
    }
    else
    {
        NSLog(@"模拟器中不能打开相机");
        [self localPhoto];
    }
}
//  选择一张照片后进入这里
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{
    NSString *type = [info objectForKey:UIImagePickerControllerMediaType];
    //  当前选择的类型是照片
    if ([type isEqualToString:@"public.image"])
    {
        // 获取照片
        getImage = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
        NSLog(@"===Decoded image size: %@", NSStringFromCGSize(getImage.size));
        // obtainImage 压缩图片 返回原尺寸
        indextNumb = indextNumb == 1?2:1;
        NSString *nameStr = [NSString stringWithFormat:@"Varify%d.jpg",indextNumb];
        [SaveImage_Util saveImage:getImage ImageName:nameStr back:^(NSString *imagePath) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"图片路径:%@",imagePath);
                /**
                 *  这里是IOS 调 js 其中 setImageWithPath 就是js中的方法 setImageWithPath(),参数是字典
                 */
                NSString *callJSString = [NSString stringWithFormat:@"%@({\"imagePath\":\"%@\",\"iosContent\":\"获取图片成功,把系统获取的图片路径传给js 让html显示\"})",@"setImageWithPath",imagePath];
                [self.webView evaluateJavaScript:callJSString completionHandler:^(id resultObject, NSError * _Nullable error) {
                    if (!error)
                    {
                        NSLog(@"OC调 JS成功");
                    }
                    else
                    {
                        NSLog(@"OC调 JS 失败");
                    }
                }];
            });
        }];
        [picker dismissViewControllerAnimated:YES completion:nil];
    }
}

@end





下面是图片处理的 工具类

//  SaveImage_Util.h
//  JS和iOS交互
//
//  Created by user on 16/10/14.
//  Copyright © 2016年 user. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface SaveImage_Util : NSObject
#pragma mark  保存图片到document
+ (BOOL)saveImage:(UIImage *)saveImage ImageName:(NSString *)imageName back:(void(^)(NSString *imagePath))back;

@end

//  SaveImage_Util.m
//  JS和iOS交互
//
//  Created by user on 16/10/14.
//  Copyright © 2016年 user. All rights reserved.
//

#import "SaveImage_Util.h"

@implementation SaveImage_Util
#pragma mark  保存图片到document
+ (BOOL)saveImage:(UIImage *)saveImage ImageName:(NSString *)imageName back:(void(^)(NSString *imagePath))back
{
    NSString *path = [SaveImage_Util getImageDocumentFolderPath];
    NSData *imageData = UIImagePNGRepresentation(saveImage);
    NSString *documentsDirectory = [NSString stringWithFormat:@"%@/", path];
    // Now we get the full path to the file
    NSString *imageFile = [documentsDirectory stringByAppendingPathComponent:imageName];
    // and then we write it out
    NSFileManager *fileManager = [NSFileManager defaultManager];
    //如果文件路径存在的话
    BOOL bRet = [fileManager fileExistsAtPath:imageFile];
    if (bRet)
    {
        //        NSLog(@"文件已存在");
        if ([fileManager removeItemAtPath:imageFile error:nil])
        {
            //            NSLog(@"删除文件成功");
            if ([imageData writeToFile:imageFile atomically:YES])
            {
                //                NSLog(@"保存文件成功");
                back(imageFile);
            }
        }
        else
        {
            
        }
        
    }
    else
    {
        if (![imageData writeToFile:imageFile atomically:NO])
        {
            [fileManager createDirectoryAtPath:documentsDirectory withIntermediateDirectories:YES attributes:nil error:nil];
            if ([imageData writeToFile:imageFile atomically:YES])
            {
                back(imageFile);
            }
        }
        else
        {
            return YES;
        }
        
    }
    return NO;
}
#pragma mark  从文档目录下获取Documents路径
+ (NSString *)getImageDocumentFolderPath
{
    NSString *patchDocument = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    return [NSString stringWithFormat:@"%@/Images", patchDocument];
}
@end

以上内容仅供参考,部分内容来之网络,如有重复,请联系修改,谢谢!

欢迎各位同志的交流,如需demo可下载(免费)


作者:Boyqicheng 发表于2016/10/14 11:21:53 原文链接
阅读:35 评论:0 查看评论

iOS10新特性

$
0
0

一、锁屏

iOS10的UI设计在总体上没有明显的变化,但是锁屏界面还是做了重新设计,锁屏状态下可以显示更丰富的通知内容,向右滑动直接进入拍照界面,左滑则有丰富的插件集成。在锁屏状态下使用3D Touch,可以直接管理通知和各项操作,无需解锁进入系统。

这里写图片描述

二、控制中心

iOS0对控制中心作出了巨大改变,首先是改进了UI设计,布局更加合理,功能更加丰富,控制中心支持左右滑动,新增音乐控制中心,使用3D Touch也可以在控制中心实现更多样化的操作。

这里写图片描述

这里写图片描述

三、Siri开放SDK

iOS10中,用户可以直接在Siri中控制第三方应用,比如搜索、查看微信消息,通过Siri呼叫滴滴打车等。首批支持Siri的国内第三方应用包括微信、滴滴Uber等。 
Siri开放SDK堪称Siri诞生以来最重要的改变,也让iOS10的系统体验进一步提升。在谷歌和微软语音助手的夹击下,Siri在智能化上明显处于劣势,iOS10的Siri还将加强学习功能,力图更加智能化,可以根据用户提供的信息主动思考,定制出用户需要的合理服务。

这里写图片描述

这里写图片描述

四、相册应用

iOS10中相册加入了智能脸部识别和场景识别能力,支持高级搜索功能,也将自动整合元素相近的照片,提供一个记忆功能面板,可以提供基于地图或不同身份的整合显示。并且新增LIvePhoto编辑功能。支持自动将相同类型图片创建视频,用户切换音乐之后,视频也会自动适应并作出相应的剪辑。

这里写图片描述

这里写图片描述

五、地图应用

苹果地图提供了更加清晰的导航界面,支持实时交通信息显示,支持长路途搜索功能,支持Car Play。苹果开放苹果地图给开发者,可以集成大众点评订餐、滴滴、Uber叫车等服务。

这里写图片描述

这里写图片描述

这里写图片描述

六、Apple Music

经常被吐槽难用的Apple Music这次大幅更新了界面,界面更加简洁,提供单独的音乐下载管理面板,底部多了浏览功能,提供全新的歌词面板显示。

七、Apple News

推送新闻分类更清晰,会根据浏览历史显示新闻,支持订阅功能以及突发新闻的通知

八、HOMEKIT

iOS10新增Home统一智能家居管理应用,通过这个应用,智能硬件的开发商基本无需自己研发APP了,使用HomeKit可以管理所有连接iOS的智能硬件。home应用提供场景化功能,也可以在锁屏状态下使用3D Touch呼出面板,快速调整智能家居设备

这里写图片描述

这里写图片描述

九、电话功能

iOS10电话功能专门为中国用户进行了优化,增加骚扰电话过滤功能,由腾讯安全提供技术支持。联系人功能加强,提供VoIP API,社交软件的联系方式可以直接添加到自带联系人中

十、iMessage

iOS10可以自动识别可以被emoji表情替换的词语,点击即可自动替换成emoji表情。iMessage支持使用触控板来发送信息,Apple Watch新增的触控板同样可以进行类似操作;支持在iMessage中使用Apple Music播放,同时iMessage正式向第三方应用开放,提供独立的程序抽屉以在iMessage中进行调用,可发送的消息不再局限于文本,也可以发送视频、图片、音乐、各种动图、支付信息,也可以添加各种动态效果;支持发送手写信息

这里写图片描述

这里写图片描述

这里写图片描述

作者:meiwenjie110 发表于2016/10/14 11:24:42 原文链接
阅读:33 评论:0 查看评论

android PakageManagerService启动流程分析

$
0
0

PakageManagerService的启动流程图

_2016_10_12_10_58_13

1.PakageManagerService概述

PakageManagerService是android系统中一个核心的服务,它负责系统中Package的管理,应该程序的安装、卸载等。后面PakageManagerService简称PMS。

2.SystemServer启动PackageManagerService

我之前的ATA文章有说到,SystemServer进程是Zygote孵化出的第一个进程,该进程主要的工作是启动android系统服务进程,其中包括PackageManagerService服务,SystemServer启动PMS关键源码如下:


    private void startBootstrapServices() {
        //...
         //调用PMS的main函数
         mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
                mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
         //判断本次是否为初次启动,当Zygote或者SystemServer退出时,init会再次启动它们,所以这里
         //的firstBoot指的是开机后的第一次启动
        mFirstBoot = mPackageManagerService.isFirstBoot();
        mPackageManager = mSystemContext.getPackageManager();
      //...
    } 

关键点

  • PMS的main函数,该函数是PKM的核心。

3.PMS的main方法

PackageManagerService的主要功能是,扫描Android系统中几个目标文件夹的APK,建立对应的数据结构来管理Package信息、四大组件信息、权限信息等各种信息。例如PKMS解析APK包中的AndroidMainfest.xml,并根据其中声明的Activity标签来创建对应的对象并加以保管。PMS的main方法的代码如下:

 public static PackageManagerService main(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        //new 一个PackageManagerService对象
        PackageManagerService m = new PackageManagerService(context, installer,
                factoryTest, onlyCore);
        //PKM注册到ServiceManager上。ServiceManager相当于安卓系统服务的DNS服务器
        ServiceManager.addService("package", m);
        return m;
 }

该方法看似很简单,只有几行代码,然而执行事件却比较长,这是因为PMS在其构造函数中做了很多的“重体力活”,这也是android启动速度慢的主要因素之一。安装的应用越多,系统启动开机时间越长。
PMS构造函数的主要工作流程

  • 扫描目标文件夹之前的准备工作。
  • 扫描目标文件夹。
  • 扫描之后的工作。

4.PMS的前期准备工作

4.1探究Setting


public PackageManagerService(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_START,
                SystemClock.uptimeMillis());

        if (mSdkVersion <= 0) {
            Slog.w(TAG, "**** ro.build.version.sdk not set!");
        }

        mContext = context;
        //是否在工厂测试模式下,假定为false
        mFactoryTest = factoryTest;
        mOnlyCore = onlyCore;
        //如果此系统是“eng”版,扫描Package后,不对package做dex优化
        mLazyDexOpt = "eng".equals(SystemProperties.get("ro.build.type"));
        //用于存储与显示屏相关的一些属性,例如屏幕的宽高分辨率等。
        mMetrics = new DisplayMetrics();
        mSettings = new Settings(mPackages);
        //第一个参数是字符串“android.uid.system”;第二个是SYSTEM_UID,其值为1000,
        //第三个是FLAG_SYSTEM标志,用于标识系统Package。
        mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.log", LOG_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

4.1.1Android系统中UID/GID

UID为用户ID的缩写,GID为用户组ID的缩写,这两个概念均与Linux系统中进程的权限管理有关。一般来说,每一个进程都有一个对应的UID,表示该进程属于哪个用户,不同用户有不同权限。一个进程也可分属不同的用户组,每个用户组都有对应的权限。
在android系统中,系统定义的UID/GID在Process.java文件中,关键源码如下所示


    /**
     * Defines the UID/GID under which system code runs.
     */
    public static final int SYSTEM_UID = 1000;

    /**
     * Defines the UID/GID under which the telephony code runs.
     */
    public static final int PHONE_UID = 1001;

    /**
     * Defines the UID/GID for the user shell.
     * @hide
     */
    public static final int SHELL_UID = 2000;

    /**
     * Defines the UID/GID for the log group.
     * @hide
     */
    public static final int LOG_UID = 1007;

    /**
     * Defines the UID/GID for the WIFI supplicant process.
     * @hide
     */
    public static final int WIFI_UID = 1010;

    /**
     * Defines the UID/GID for the mediaserver process.
     * @hide
     */
    public static final int MEDIA_UID = 1013;

    /**
     * Defines the UID/GID for the DRM process.
     * @hide
     */
    public static final int DRM_UID = 1019;

    /**
     * Defines the UID/GID for the group that controls VPN services.
     * @hide
     */
    public static final int VPN_UID = 1016;

    /**
     * Defines the UID/GID for the NFC service process.
     * @hide
     */
    public static final int NFC_UID = 1027;

    /**
     * Defines the UID/GID for the Bluetooth service process.
     * @hide
     */
    public static final int BLUETOOTH_UID = 1002;

    /**
     * Defines the GID for the group that allows write access to the internal media storage.
     * @hide
     */
    public static final int MEDIA_RW_GID = 1023;

    /**
     * Access to installed package details
     * @hide
     */
    public static final int PACKAGE_INFO_GID = 1032;

    /**
     * Defines the UID/GID for the shared RELRO file updater process.
     * @hide
     */
    public static final int SHARED_RELRO_UID = 1037;

    /**
     * Defines the start of a range of UIDs (and GIDs), going from this
     * number to {@link #LAST_APPLICATION_UID} that are reserved for assigning
     * to applications.
     */
    public static final int FIRST_APPLICATION_UID = 10000;

    /**
     * Last of application-specific UIDs starting at
     * {@link #FIRST_APPLICATION_UID}.
     */
    public static final int LAST_APPLICATION_UID = 19999;

    /**
     * First uid used for fully isolated sandboxed processes (with no permissions of their own)
     * @hide
     */
    public static final int FIRST_ISOLATED_UID = 99000;

    /**
     * Last uid used for fully isolated sandboxed processes (with no permissions of their own)
     * @hide
     */
    public static final int LAST_ISOLATED_UID = 99999;
4.1.2 探究SharedUserSetting

Setting中有一个mShareUsers成员,该成员存储的是字符串变量name与SharedUserSetting健值对。


SharedUserSetting addSharedUserLPw(String name, int uid, int pkgFlags, int pkgPrivateFlags) {
        //mSharedUsers是一个HashMap.key为字符串,值为ShareUserSetting对象
        SharedUserSetting s = mSharedUsers.get(name);
        if (s != null) {
            if (s.userId == uid) {
                return s;
            }
           //...
                       return null;
        }
        创建一个SharedUserSetting对象,并设置为userid为uid
        s = new SharedUserSetting(name, pkgFlags, pkgPrivateFlags);
        s.userId = uid;
        if (addUserIdLPw(uid, s, name)) {
            mSharedUsers.put(name, s);
            return s;
        }
        return null;
    }

例如在SystemUI.apk的AndroidManifest.xml文件中,有关键代码:

<mainfest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.android.systemui"
        coreApp="true"
        android:sharedUserId="android.uid.system"
        android:process="system">
        ....

在该标签中,声明了一个android:sharedUserId的属性,其值为“android.uid.system”。sharedUserId和UID有关,它的作用是

  • 两个或者多个声明了同一种sharedUserid的APK可共享彼此的数据,并且可运行在同一进程中。
  • 通过声明特定的sharedUserId,该APK所在的进程将被赋予指定UID。

例如SystemUI声明了system的uid,运行SystemUI的进程就可享有system用户所对应的权限了,实际上就是将该进程的UID设置为system的uid了

接下来分析addUserIdLPw的功能,它主要就是将SharedUserSettings对象保存到对应的数组中,代码如下

private boolean addUserIdLPw(int uid, Object obj, Object name) {
        //uid不能超出限制,Android对uid进行归纳,系统APK所在进程小于10000
        //应用APK所在进程的uid从10000开始
        if (uid > Process.LAST_APPLICATION_UID) {
            return false;
        }
        //FIRST_APPLICATION_UID = 10000,属于应用APK
        if (uid >= Process.FIRST_APPLICATION_UID) {
            int N = mUserIds.size();
            //计算索引,其值是uid和FIRST_APPLICATION_UID的差
            final int index = uid - Process.FIRST_APPLICATION_UID;
            while (index >= N) {
                mUserIds.add(null);
                N++;
            }
            //如果索引位置不为空,返回
            if (mUserIds.get(index) != null) {
                PackageManagerService.reportSettingsProblem(Log.ERROR,
                        "Adding duplicate user id: " + uid
                        + " name=" + name);
                return false;
            }
            //mUserIds保存应用Package的uid,obj是SharedUserSettings
            mUserIds.set(index, obj);
        } else {
            if (mOtherUserIds.get(uid) != null) {
                PackageManagerService.reportSettingsProblem(Log.ERROR,
                        "Adding duplicate shared id: " + uid
                                + " name=" + name);
                return false;
            }
            mOtherUserIds.put(uid, obj);
        }
        return true;
    }

4.2 XML文件扫描

接下来是扫描系统目录下与系统权限相关的xml文件,将其存放到PKM中,关键源码如下:

        // 获取系统相关的权限,它主要是解析系统目录下xml文件,获得设备相关的权限
        SystemConfig systemConfig = SystemConfig.getInstance();
        mGlobalGids = systemConfig.getGlobalGids();
        mSystemPermissions = systemConfig.getSystemPermissions();
        mAvailableFeatures = systemConfig.getAvailableFeatures();

        synchronized (mInstallLock) {
        // writer
        synchronized (mPackages) {
            //创建一个ThreadHandler对象,实际就是创建一个带消息队列循环处理的线程,
            //该线程的工作是:程序的安装和卸载等。
            mHandlerThread = new ServiceThread(TAG,
                    Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
            mHandlerThread.start();
            //以ThreadHandler线程的消息循环(Looper对象)作为参数new一个
            //PackageHandler,因此该Handler的handlemessage方法将运行在此线程上
            mHandler = new PackageHandler(mHandlerThread.getLooper());
            Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);
            // /data目录
            File dataDir = Environment.getDataDirectory();
            // /data/data目录
            mAppDataDir = new File(dataDir, "data");
            // /data/app目录
            mAppInstallDir = new File(dataDir, "app");
            // /data/app-lib目录
            mAppLib32InstallDir = new File(dataDir, "app-lib");
            // /data/app-asec目录            
            mAsecInternalPath = new File(dataDir, "app-asec").getPath();
            // /data/user目录            
            mUserAppDataDir = new File(dataDir, "user");
            // /data/app-private目录            
            mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
            //new一个UserManager对象,目前没有什么作用,但其前途不可限量。
            //google设想,未来手机将支持多个User,每个User安装自己的应用
            //该功能为android智能手机推向企业用户打下基础
            sUserManager = new UserManagerService(context, this,
                    mInstallLock, mPackages);

            // 获取系统相关的权限,它主要是解析系统目录下xml文件,获得设备相关的权限
            ArrayMap<String, SystemConfig.PermissionEntry> permConfig
                    = systemConfig.getPermissions();
            for (int i=0; i<permConfig.size(); i++) {
                SystemConfig.PermissionEntry perm = permConfig.valueAt(i);
                BasePermission bp = mSettings.mPermissions.get(perm.name);
                if (bp == null) {
                    bp = new BasePermission(perm.name, "android", BasePermission.TYPE_BUILTIN);
                    mSettings.mPermissions.put(perm.name, bp);
                }
                if (perm.gids != null) {
                    bp.setGids(perm.gids, perm.perUser);
                }
            }
            //获得系统的Libraries,也就是系统的一些jar
            ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
            for (int i=0; i<libConfig.size(); i++) {
                mSharedLibraries.put(libConfig.keyAt(i),
                        new SharedLibraryEntry(libConfig.valueAt(i), null));
            }

            mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();

            mRestoredSettings = mSettings.readLPw(this, sUserManager.getUsers(false),
                    mSdkVersion, mOnlyCore);

            String customResolverActivity = Resources.getSystem().getString(
                    R.string.config_customResolverActivity);
            if (TextUtils.isEmpty(customResolverActivity)) {
                customResolverActivity = null;
            } else {
                mCustomResolverComponentName = ComponentName.unflattenFromString(
                        customResolverActivity);
            }

            long startTime = SystemClock.uptimeMillis();

进一步我们再观察SystemConfig是如何解析系统权限xml文件的,在SystemConfig的构造函数中,它会去分别读取etc目录下的sysconfig,permissions,sysconfig目录下的文件。

        SystemConfig() {
        // Read configuration from system
        readPermissions(Environment.buildPath(
                Environment.getRootDirectory(), "etc", "sysconfig"), false);
        // Read configuration from the old permissions dir
        readPermissions(Environment.buildPath(
                Environment.getRootDirectory(), "etc", "permissions"), false);
        // Only read features from OEM config
        readPermissions(Environment.buildPath(
                Environment.getOemDirectory(), "etc", "sysconfig"), true);
        readPermissions(Environment.buildPath(
                Environment.getOemDirectory(), "etc", "permissions"), true);
    }

我们看看到底这些目录下放着什么样的文件,例如/etc/permissions目录下的文件如下图:
Screenshot_2016_10_10_15_35_50

我们再打开第一个文件来探究,没错,这个文件代表蓝牙权限,表示该设备支持蓝牙。具体代码如下

<?xml version="1.0" encoding="utf-8"?>

<permissions>
    <feature name="android.hardware.bluetooth" />
</permissions>

总结一下PMS的前期工作,其实就是扫描并解析XML文件,将其中的信息保存到特定的数据结构中。

5.PMS扫描Package

第二个阶段的工作主要是扫描系统中的APK,由于需要逐个扫描apk文件,因此手机上安装的程序越多,PKM的工作量越大,系统启动速度越慢,也就是开机时间越长。

5.1系统库的dex优化

以下的代码主要是对系统库BOOTCLASSPATH指定,或platform.xml定义,或者/system/frameworks目录下的jar
和apk包进行一次检查,该dex优化的优化.dex优化后会在相应的目录生成.odex文件。/system/frameworks如下图:
Screenshot_2016_10_11_19_12_30
Screenshot_2016_10_11_19_41_03
关键源码如下:

            // Set flag to monitor and not change apk file paths when
            // scanning install directories.
            //定义扫描参数
            final int scanFlags = SCAN_NO_PATHS | SCAN_DEFER_DEX | SCAN_BOOTING | SCAN_INITIAL;
            //用于存储已经dex优化过或者不需要优化的包
            final ArraySet<String> alreadyDexOpted = new ArraySet<String>();

            /**
             * Add everything in the in the boot class path to the
             * list of process files because dexopt will have been run
             * if necessary during zygote startup.
             */
             //获取java启动类库的路径,在init.rc文件中通过BOOTCLASSPATH环境变量输出
             //主要是/system/framework/下的系统jar包
            final String bootClassPath = System.getenv("BOOTCLASSPATH");
            final String systemServerClassPath = System.getenv("SYSTEMSERVERCLASSPATH");
            if (bootClassPath != null) {
                String[] bootClassPathElements = splitString(bootClassPath, ':');
                //循环遍历/system/framework/下的系统jar包的绝对路径,添加到alreadyDexOpted
                for (String element : bootClassPathElements) {
                    alreadyDexOpted.add(element);
                }
            } 
            if (mSharedLibraries.size() > 0) {
                //...
                 if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) {
                                alreadyDexOpted.add(lib);
                                //dex优化
                                mInstaller.dexopt(lib, Process.SYSTEM_UID, true, dexCodeInstructionSet, dexoptNeeded);        

                }
            }

            //...

            File frameworkDir = new File(Environment.getRootDirectory(), "framework");
            //framework-res.apk定义了系统常用的资源,还有几个重要的Activity,如长按Power键弹出选择框
            //不需要dex优化
            alreadyDexOpted.add(frameworkDir.getPath() + "/framework-res.apk");

            alreadyDexOpted.add(frameworkDir.getPath() + "/core-libart.jar");

            //扫描framework/下的apk或者jar进行dex优化
            String[] frameworkFiles = frameworkDir.list();
            if (frameworkFiles != null) {
                // TODO: We could compile these only for the most preferred ABI. We should
                // first double check that the dex files for these commands are not referenced
                // by other system apps.
                for (String dexCodeInstructionSet : dexCodeInstructionSets) {
                    for (int i=0; i<frameworkFiles.length; i++) {
                        File libPath = new File(frameworkDir, frameworkFiles[i]);
                        String path = libPath.getPath();
                        // 跳过已经存在的包
                        if (alreadyDexOpted.contains(path)) {
                            continue;
                        }
                        // 不是apk或者jar的不做处理
                        if (!path.endsWith(".apk") && !path.endsWith(".jar")) {
                            continue;
                        }
                        int dexoptNeeded = DexFile.getDexOptNeeded(path, null, dexCodeInstructionSet, false);
                        if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) {
                            //dex优化
                           mInstaller.dexopt(path, Process.SYSTEM_UID, true, dexCodeInstructionSet, dexoptNeeded);
                            }

                    }
                }
            }

5.2扫描系统的APK

对apk或者jar进行dex优化后,现在PKM进入了重点阶段,扫描系统的APK,每一个APK对应一个Package对象,主要是扫描APK的AndroidManifest.xml,解析application标签及其子标签actvity、service、recever等,也就是android的四大组件,解析后将它们保存到Package对应的数据结构中,最后将它们注册到PKM中,要扫描以下几个目录:

  • /system/frameworks:该目录下的文件都是系统库,例如service.jar、framework.jar、framework-res.apk。不过只扫描framework-res.apk文件
  • /system/app:该目录下全是默认的系统应用和厂商特定的APK文件,例如Buletooth.apk、和SystemUI.apk等

解析AndroidManifest.xml关键的源码如下:

private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags,
            String[] outError) throws XmlPullParserException, IOException {

             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                continue;
            }

            String tagName = parser.getName();
            //解析<application>
            if (tagName.equals("application")) {
                if (foundApp) {
                    if (RIGID_PARSER) {
                        outError[0] = "<manifest> has more than one <application>";
                        mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                        return null;
                    } else {
                        Slog.w(TAG, "<manifest> has more than one <application>");
                        XmlUtils.skipCurrentTag(parser);
                        continue;
                    }
                }

                foundApp = true;
                //解析<application>及其子标签
                if (!parseBaseApplication(pkg, res, parser, attrs, flags, outError)) {
                    return null;
                }
            //解析<application>
            } else if (tagName.equals("overlay")) {

            } else if (tagName.equals("key-sets")) {

            } else if (tagName.equals("permission-group")) {

            } else if (tagName.equals("permission")) {

            } else if (tagName.equals("permission-tree")) {
            //解析<uses-permission>
            } else if (tagName.equals("uses-permission")) {

            } else if (tagName.equals("uses-permission-sdk-m")
                    || tagName.equals("uses-permission-sdk-23")) {

            } else if (tagName.equals("uses-configuration")) {

            } else if (tagName.equals("uses-feature")) {

            } else if (tagName.equals("feature-group")) {

            } else if (tagName.equals("uses-sdk")) {

            } else if (tagName.equals("supports-screens")) {

            } else if (tagName.equals("instrumentation")) {

            } else if (tagName.equals("original-package")) {

            } else if (tagName.equals("adopt-permissions")) {

            } else if (tagName.equals("uses-gl-texture")) {

            } else if (tagName.equals("compatible-screens")) {

            } else if (tagName.equals("supports-input")) {

            } else if (tagName.equals("eat-comment")) {

            } else if (RIGID_PARSER) {


            } else {

            }
        }
}

具体看一下怎么解析application标签下的四大组件的,依次解析activity,receiver,service,provider,其中可以发现,receiver被当成activity来解析了,PKM通过PackageParser类将解析后的四大组件保存到对应数据结构中,也就是存放到PackageParser的activities,receivers,providers,services对象中。关键源码如下:

private boolean parseBaseApplication(Package owner, Resources res,
            XmlPullParser parser, AttributeSet attrs, int flags, String[] outError){
            //...
 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                continue;
            }

            String tagName = parser.getName();
            //activity标签
            if (tagName.equals("activity")) {
                //解析activity标签
                Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false,
                        owner.baseHardwareAccelerated);
                //添加activity到owner.activities中
                owner.activities.add(a);
            //receiver标签
            } else if (tagName.equals("receiver")) {
                //解析receiver标签,receiver其实被当成Activity来解析了。
                Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true, false);
                if (a == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }
            //添加activity到owner.activities中
                owner.receivers.add(a);
            //service标签
            } else if (tagName.equals("service")) {
                //解析service标签
                Service s = parseService(owner, res, parser, attrs, flags, outError);
                if (s == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }
            //添加service到owner.services中
                owner.services.add(s);
            //provider标签
            } else if (tagName.equals("provider")) {
                Provider p = parseProvider(owner, res, parser, attrs, flags, outError);
                if (p == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.providers.add(p);

            } else if (tagName.equals("activity-alias")) {
            } else if (parser.getName().equals("meta-data")) {

            } else if (tagName.equals("uses-library")) {


            } else if (tagName.equals("uses-package")) {
            }
        }            
}

在PackageParser扫描完一个APK后,此时系统已经根据APK中的AndroidMainifest.xml,创建了一个Package对象,下一步是将该Package加入到系统中。此时调用scanPackageDirtyLI方法,scanPackageDirtyLI首先会对packageName为“android”的apk做单独的处理,该apk其实就是framework-res.apk,它包含了几个常见的activity

  • ChooserActivity:当startActivity有多个Acitvity符合时,系统会弹出此Acitivity,由用户选择合适的应用来处理
  • ShutDownActivity:关机前弹出的选择对话框
  • RingtonePickerAcitivity:铃声选择Activity

scanPackageDirtyLI关键代码如下:

private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg, int parseFlags,
            int scanFlags, long currentTime, UserHandle user) throws PackageManagerException {
        final File scanFile = new File(pkg.codePath);
        if (pkg.applicationInfo.getCodePath() == null ||
                pkg.applicationInfo.getResourcePath() == null) {
            // Bail out. The resource and code paths haven't been set.
            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                    "Code and resource paths haven't been set correctly");
        }

        if ((parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0) {
            pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
        } else {
            // Only allow system apps to be flagged as core apps.
            pkg.coreApp = false;
        }

        if ((parseFlags&PackageParser.PARSE_IS_PRIVILEGED) != 0) {
            pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
        }

        if (mCustomResolverComponentName != null &&
                mCustomResolverComponentName.getPackageName().equals(pkg.packageName)) {
            setUpCustomResolverActivity(pkg);
        }

        if (pkg.packageName.equals("android")) {
            synchronized (mPackages) {
                if (mAndroidApplication != null) {
                   //...  
                }

                //保存该package信息
                mPlatformPackage = pkg;
                pkg.mVersionCode = mSdkVersion;
                //保存该package的ApplicationInfo
                mAndroidApplication = pkg.applicationInfo;

                if (!mResolverReplaced) {
                    //mResolveActivity为ChooserActivity信息的ActivityInfo
                    mResolveActivity.applicationInfo = mAndroidApplication;
                    mResolveActivity.name = ResolverActivity.class.getName();
                    mResolveActivity.packageName = mAndroidApplication.packageName;
                    mResolveActivity.processName = "system:ui";
                    mResolveActivity.launchMode = ActivityInfo.LAUNCH_MULTIPLE;
                    mResolveActivity.documentLaunchMode = ActivityInfo.DOCUMENT_LAUNCH_NEVER;
                    mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;
                    mResolveActivity.theme = R.style.Theme_Holo_Dialog_Alert;
                    mResolveActivity.exported = true;
                    mResolveActivity.enabled = true;
                    //mResolveInfo用于存储系统解析Intent后得到的结果信息,在从PKM查询满足某个Intent的
                    //Activity时,返回的就是ResolveInfo,再根据ResolveInfo的activityInfo的信息得到                         //Activity
                    mResolveInfo.activityInfo = mResolveActivity;
                    mResolveInfo.priority = 0;
                    mResolveInfo.preferredOrder = 0;
                    mResolveInfo.match = 0;
                    mResolveComponentName = new ComponentName(
                            mAndroidApplication.packageName, mResolveActivity.name);
                }
            }
        }
 }

“android“该Package与系统有非常重要的作用,这里保存特殊处理保存该Package的信息,主要是为了提高运行过程中的效率,例如ChooserActivity使用的地方非常多。
接下里scanPackageDirtyLI方法会对系统其它的Package做处理,关键源码如下:

//mPackages用于保存系统内的所有Package,以packageName为key
 if (mPackages.containsKey(pkg.packageName)
                || mSharedLibraries.containsKey(pkg.packageName)) {
           //...
                   }

        if ((scanFlags & SCAN_REQUIRE_KNOWN) != 0) {
            if (mExpectingBetter.containsKey(pkg.packageName)) {
                logCriticalInfo(Log.WARN,
                        "Relax SCAN_REQUIRE_KNOWN requirement for package " + pkg.packageName);
            } else {
                PackageSetting known = mSettings.peekPackageLPr(pkg.packageName);
                if (known != null) {
                    if (DEBUG_PACKAGE_SCANNING) {
                        Log.d(TAG, "Examining " + pkg.codePath
                                + " and requiring known paths " + known.codePathString
                                + " & " + known.resourcePathString);
                    }
                    if (!pkg.applicationInfo.getCodePath().equals(known.codePathString)
                            || !pkg.applicationInfo.getResourcePath().equals(known.resourcePathString)) {
                        throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED,
                                "Application package " + pkg.packageName
                                + " found at " + pkg.applicationInfo.getCodePath()
                                + " but expected at " + known.codePathString + "; ignoring.");
                    }
                }
            }
        }

        // Initialize package source and resource directories
        File destCodeFile = new File(pkg.applicationInfo.getCodePath());
        File destResourceFile = new File(pkg.applicationInfo.getResourcePath());

        SharedUserSetting suid = null;
        PackageSetting pkgSetting = null;

        if (!isSystemApp(pkg)) {
            // Only system apps can use these features.
            pkg.mOriginalPackages = null;
            pkg.mRealPackage = null;
            pkg.mAdoptPermissions = null;
        }
        //...
        final String pkgName = pkg.packageName;

        final long scanFileTime = scanFile.lastModified();
        final boolean forceDex = (scanFlags & SCAN_FORCE_DEX) != 0;
        //确定运行该package的进程名,一般用package作为进程名
        pkg.applicationInfo.processName = fixProcessName(
                pkg.applicationInfo.packageName,
                pkg.applicationInfo.processName,
                pkg.applicationInfo.uid);

        File dataPath;
        if (mPlatformPackage == pkg) {
            // The system package is special.
            dataPath = new File(Environment.getDataDirectory(), "system");

            pkg.applicationInfo.dataDir = dataPath.getPath();

        } else {
            // This is a normal package, need to make its data directory.
            //该函数返回data/data/packageName
            dataPath = Environment.getDataUserPackageDirectory(pkg.volumeUuid,
                    UserHandle.USER_OWNER, pkg.packageName);

            boolean uidError = false;
            if (dataPath.exists()) {
                //..
            } else {
                if (DEBUG_PACKAGE_SCANNING) {
                    if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
                        Log.v(TAG, "Want this data dir: " + dataPath);
                }
                //该方法调用installer发送install命令,其实就是在/data/data/目录下建立packageName目录
                //然后为系统所有的user安装此apk
                int ret = createDataDirsLI(pkg.volumeUuid, pkgName, pkg.applicationInfo.uid,
                        pkg.applicationInfo.seinfo);
                //安装错误
                if (ret < 0) {
                    // Error from installer
                    throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE,
                            "Unable to create data dirs [errorCode=" + ret + "]");
                }

            }

            pkgSetting.uidError = uidError;
        }

        final String path = scanFile.getPath();
        final String cpuAbiOverride = deriveAbiOverride(pkg.cpuAbiOverride, pkgSetting);
        //在/data/data/pageName/lib下建立和CPU类型对应的目录,例如ARM平台的事arm/,MIP平台的事mips/
        if ((scanFlags & SCAN_NEW_INSTALL) == 0) {
            derivePackageAbi(pkg, scanFile, cpuAbiOverride, true /* extract libs */);

            //系统package的native库统一放在/system/lib下,
            //所以系统不会提取系统package目录apk包中的native库
            if (isSystemApp(pkg) && !pkg.isUpdatedSystemApp() &&
                    pkg.applicationInfo.primaryCpuAbi == null) {
                setBundledAppAbisAndRoots(pkg, pkgSetting);
                setNativeLibraryPaths(pkg);
            }

        } else {
            if ((scanFlags & SCAN_MOVE) != 0) {
            //
            setNativeLibraryPaths(pkg);
        }
        //...
        if ((scanFlags & SCAN_NO_DEX) == 0) {
            //对该APK做dex优化
            int result = mPackageDexOptimizer.performDexOpt(pkg, null /* instruction sets */,
                    forceDex, (scanFlags & SCAN_DEFER_DEX) != 0, false /* inclDependencies */);
      //如果该apk已经存在,要先杀掉该APK的进程
         if ((scanFlags & SCAN_REPLACING) != 0) {
            killApplication(pkg.applicationInfo.packageName,
                        pkg.applicationInfo.uid, "replace pkg");
        }
        //在此之前,四大组件信息都是Package对象的私有的,在这里把它们注册到PKM内部的财产管理对象中。
        //这样,PKMS就可对外提供统一的组件信息。
        synchronized (mPackages) {
                ...
            //注册该Package中的provider到PKM的mProviders上
            int N = pkg.providers.size();
            StringBuilder r = null;
            int i;
            for (i=0; i<N; i++) {
                PackageParser.Provider p = pkg.providers.get(i);
                p.info.processName = fixProcessName(pkg.applicationInfo.processName,
                        p.info.processName, pkg.applicationInfo.uid);
                mProviders.addProvider(p);
                p.syncable = p.info.isSyncable;
                if (p.info.authority != null) {
                   //...           
                    }
            //注册该Package中的service到PKM的mServices上
            N = pkg.services.size();
            r = null;
            for (i=0; i<N; i++) {
                PackageParser.Service s = pkg.services.get(i);
                s.info.processName = fixProcessName(pkg.applicationInfo.processName,
                        s.info.processName, pkg.applicationInfo.uid);
                mServices.addService(s);
                if ((parseFlags&PackageParser.PARSE_CHATTY) != 0) {
                    if (r == null) {
                        r = new StringBuilder(256);
                    } else {
                        r.append(' ');
                    }
                    r.append(s.info.name);
                }
            }
            if (r != null) {
                if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, "  Services: " + r);
            }
            //注册该Package中的receiver到PKM的mReceivers上
            N = pkg.receivers.size();
            r = null;
            for (i=0; i<N; i++) {
                PackageParser.Activity a = pkg.receivers.get(i);
                a.info.processName = fixProcessName(pkg.applicationInfo.processName,
                        a.info.processName, pkg.applicationInfo.uid);
                mReceivers.addActivity(a, "receiver");
                if ((parseFlags&PackageParser.PARSE_CHATTY) != 0) {
                    if (r == null) {
                        r = new StringBuilder(256);
                    } else {
                        r.append(' ');
                    }
                    r.append(a.info.name);
                }
            }
            if (r != null) {
                if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, "  Receivers: " + r);
            }
            //注册该Package中的activity到PKM的mActivities上
            N = pkg.activities.size();
            r = null;
            for (i=0; i<N; i++) {
                PackageParser.Activity a = pkg.activities.get(i);
                a.info.processName = fixProcessName(pkg.applicationInfo.processName,
                        a.info.processName, pkg.applicationInfo.uid);
                mActivities.addActivity(a, "activity");
                if ((parseFlags&PackageParser.PARSE_CHATTY) != 0) {
                    if (r == null) {
                        r = new StringBuilder(256);
                    } else {
                        r.append(' ');
                    }
                    r.append(a.info.name);
                }
            }
            if (r != null) {
                if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, "  Activities: " + r);
            }
            //...
          return pkg;
    }

5.3扫描非系统apk

在PackageManagerService构造函数扫描完系统apk后,接下来就是扫描非系统apk,这些apk在/data/app或者/data/app-private中。如下图:
Screenshot_2016_10_11_19_41_42
下面是关键源码,scanDirLI已经在前面分析过了。跟系统apk的调用过程差不多。

        if (!mOnlyCore) {
                EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
                        SystemClock.uptimeMillis());
                scanDirLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);

                scanDirLI(mDrmAppPrivateInstallDir, PackageParser.PARSE_FORWARD_LOCK,
                        scanFlags | SCAN_REQUIRE_KNOWN, 0);
        }

5.4扫描结果保存到文件中

在PackageManagerService构造函数收尾阶段,PMS将前面收集的信息再整理一次,将已安装的apk信息写到package.xml、pacakage.list和package-stopped.xml中

            //整理更新Permisssion的信息
            updatePermissionsLPw(null, null, updateFlags);
            //...

            //将信息写到package.xml,pacakage.list和package-stopped.xml中
            mSettings.writeLPr();

            EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_READY,
                    SystemClock.uptimeMillis());

            mRequiredVerifierPackage = getRequiredVerifierLPr();
            mRequiredInstallerPackage = getRequiredInstallerLPr();

        } // synchronized (mPackages)
        } // synchronized (mInstallLock)

        //gc
        Runtime.getRuntime().gc();
  • packages.xml:系统对程序安装,卸载和更新等操作时会更新该文件,PMS扫描完目标文件夹后创建该文件,保存了Package相关的信息。
  • packages.list:保存着系统中所有的非系统自带的APK信息,程序安装,卸载和更新会更新该文件。
  • packages-stoped.xml:保存系统中被用户强制停止的Package的信息。

5.4扫描系统和非系统apk总结

PKM在这个过程中工作任务非常繁重,要创建很多的对象,所以它是一个耗时耗内存的操作,从流程来看,PKM在这个过程中无非是扫描XML或者APK文件,但是其中涉及的数据结构及它们的关系较为复杂。

作者:xiangzhihong8 发表于2016/10/14 11:41:51 原文链接
阅读:35 评论:0 查看评论

细说Android框架设计三剑客MVC、MVP和MVVM

$
0
0

    最近几年的移动端开发越来越火,功能越来越强大,处理业务越来越复杂,因此对系统扩展性的要求越来越高。而为了更好地进行移动端架构设计,我们最常用的就是MVC和MVP,今天本篇博客就和大家一起聊一聊这两种框架设计。

MVC框架

MVC的定义

    MVC (Model-View-Controller):M是指逻辑模型,V是指视图模型,C则是控制器。使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式,而C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新,这与《设计模式》中的观察者模式是完全一样。

为何用MVC

  • 从用户的角度出发,用户可以根据自己的需求,选择自己合适的浏览数据的方式。
  • 从开发者的角度,MVC把应用程序的逻辑层与界面是完全分开的,这样,界面设计人员可以直接参与到界面开发,程序员就可以把精力放在逻辑层上。而不是像以前那样,设计人员把所有的材料交给开发人员,由开发人员来实现界面。

MVC的通信方式

这里写图片描述

首先,View发送命令到Controller,然后Controller处理完业务逻辑后让Model改变状态,最后由Model将新的数据发送到View,用户得到数据响应。

Android中的MVC

    1.视图层(View):一般采用XML文件进行界面的描述,使用的时候可以非常方便的引入。
    2.控制层(Controller):Android的控制层通常是在Acitvity中实现。
    3.模型层(Model):对数据库的操作、对网络等的操作都应该在Model里面处理,当然对业务计算等操作也是必须放在的该层的。

MVP框架

MVP的定义

    MVC (Model-View-Presenter):MVP其实是由MVC演变而来的,其中的M依然是指逻辑模型,V依然是指视图模型,而P(中间桥梁)则代替了C成为了逻辑控制器的角色。

MVC和MVP到底有啥区别

    区别就在于MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。我们知道在MVC里,View是可以直接访问Model的。从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,即View。所以,在MVC模型里,Model不依赖于View,但是View是依赖于Model的。

MVP的通信方式

这里写图片描述

    首先MVP各部分之间的通信都是双向的,但是唯独View与Model之间是不发生联系的,二者之间的通信都是通过Presenter传递的。在MVP里,应用程序的逻辑主要在Presenter来实现,其中的View是很薄的一层。在这个过程中,View是很简单的,能够把信息显示清楚就可以了。在后面,根据需要再随便更改View,而对Presenter没有任何的影响了。 如果要实现的UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在View和Presenter之间放置一个Adapter。由这个 Adapter来访问Model和View,避免两者之间的关联。而同时,因为Adapter实现了View的接口,从而可以保证与Presenter之间接口的不变。这样就可以保证View和Presenter之间接口的简洁,又不失去UI的灵活性。 在MVP模式里,View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容,绝不容许直接访问Model–这就是与MVC很大的不同之处。

MVP的优缺点

以下内容来自百度百科

1、模型与视图完全分离,我们可以修改视图而不影响模型
2、可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部
3、我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。

MVVM框架

MVVM的定义

    MVC (Model-View-ViewModel):MVVM和MVP的区别其实不大,只不过是把presenter层换成了ViewModel层,再有就是View层和ViewModel层是相互绑定的关系,当我们更新ViewModel层的数据的时候,View层会相应的更新UI。

MVVM的通信方式

这里写图片描述
    MVVM它采用的是数据绑定(data-binding)方式,而且是双向绑定:View绑定到ViewModel,然后执行一些命令在向它请求一个动作。而反过来,ViewModel跟Model通讯,告诉它更新来响应UI。

MVVM优点

以下内容来自百度百科
MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点

  1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
  3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
  4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

MVP案例实践

    下面我选取MVP框架作为主要案例,给大家讲解如何将一个普通项目改造成MVP框架模式的项目,帮助大家理解MVP框架模式的意义。

传统项目结构

这里写图片描述

该项目中我们定义了一个学生信息列表,用来显示图片和文字;项目源码如下:

MainActivity.java

/**
 * 功能:MainActivity 用列表形式实现
 * 作者:猴子搬来的救兵
 * 博客地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class MainActivity extends AppCompatActivity implements IStudentView{

    ListView myList;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myList = (ListView) findViewById(R.id.my_list);
        myList.setAdapter(new StudentAdapter(MainActivity.this));
    }
}

StudentAdapter.java

/**
 * 功能:StudentAdapter
 * 作者:猴子搬来的救兵
 * 博客地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class StudentAdapter extends BaseAdapter {

    private LayoutInflater myInflater;
    private List<Student> data;

    public StudentAdapter(Context context, List<Student> data) {
        myInflater = LayoutInflater.from(context);
        this.data = data;
    }

    @Override
    public int getCount() {
        return DataUtils.stuSize();
    }

    @Override
    public Object getItem(int position) {
        return DataUtils.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View stuView = myInflater.inflate(R.layout.stu_item, null);
        Student student = DataUtils.get(position);
        ImageView imgStu = (ImageView) stuView.findViewById(R.id.img_stu);
        imgStu.setImageResource(student.getStuImg());
        TextView tvStu = (TextView) stuView.findViewById(R.id.tv_name);
        tvStu.setText(student.getName());
        return stuView;
    }

}

DataUtils.java

/**
 * 功能:初始化数据工具类
 * 作者:猴子搬来的救兵
 * 博客地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class DataUtils {

    public final static List<Student> stuData = new ArrayList<Student>();

    public static Student get(int i){
        return stuData.get(i);
    }

    public static int stuSize(){
        return stuData.size();
    }

    static {
        stuData.add(new Student("张三",R.mipmap.ic_launcher));
        stuData.add(new Student("李四",R.mipmap.ic_launcher));
        stuData.add(new Student("王五",R.mipmap.ic_launcher));
        stuData.add(new Student("赵六",R.mipmap.ic_launcher));
        stuData.add(new Student("陈七",R.mipmap.ic_launcher));
        stuData.add(new Student("孙八",R.mipmap.ic_launcher));
        stuData.add(new Student("猴子搬来的救兵",R.mipmap.ic_launcher));

    }
}

Student.java

/**
 * 功能:学生Bean
 * 作者:猴子搬来的救兵
 * 博客地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class Student {

    private String name;
    private int stuImg;

    public Student() {
    }

    public Student(String name, int stuImg) {
        this.name = name;
        this.stuImg = stuImg;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStuImg() {
        return stuImg;
    }

    public void setStuImg(int stuImg) {
        this.stuImg = stuImg;
    }

}

项目运行结果:

这里写图片描述

MVP改造结构

这里写图片描述

改造后的项目,我们增加了三大模块,分别是:

  • Model包负责处理数据
  • View包负责显示处理
  • Presenter包是中间桥梁,负责Model和View的交互

通过MVP框架实现数据适配

定义Model层接口

public interface IStudentModel {
    // 加载数据
    void loadStudent(StudentOnLoadListener listener);
    interface StudentOnLoadListener{
       void onComplete(List<Student> students);
    }
}

定义View层接口

public interface IStudentView {
    // 显示进度
    void showLoading();
    // 显示学生
    void showStudents(List<Student> students);
}

添加Model层

/**
 * 功能:StudentModel 第一次数据处理
 * 作者:猴子搬来的救兵
 * 博客地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/13 0013
 */
public class StudentModelImplOne implements IStudentModel {
    @Override
    public void loadStudent(StudentOnLoadListener listener) {
        Log.i("castiel","执行了StudentModelImplOne数据加载");
        //模拟Json数据
        List<Student> jsonStu1 = new ArrayList<Student>();
        jsonStu1.add(new Student("张三11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("李四11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("王五11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("赵六11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("陈七11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("孙八11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("猴子搬来的救兵11http://blog.csdn.net/mynameishuangshuai", R.mipmap.ic_launcher));
        // 通过回调方式传递数据
        if (listener != null) {
            listener.onComplete(jsonStu1);
        }
    }
}

添加Presenter层

public class StudentPresenterOne {
    // Model
   IStudentModel mStudentModel = new StudentModelImplOne();
    // View
    IStudentView mStudentView;
    // 初始化View
    public StudentPresenterOne(IStudentView mStudentView) {
        this.mStudentView = mStudentView;
    }
    public void fetch(){
        mStudentView.showLoading();// 显示进度
        // Model获取数据
        if (mStudentModel != null){
            mStudentModel.loadStudent(new IStudentModel.StudentOnLoadListener() {
                @Override
                public void onComplete(List<Student> students) {
                    // 得到数据后给View显示数据
                    mStudentView.showStudents(students);
                }
            });
        }
    }
}

执行操作

/**
 * 功能:MainActivity 用列表形式实现
 * 作者:猴子搬来的救兵
 * 博客地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class MainActivity extends AppCompatActivity implements IStudentView{

    ListView myList;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myList = (ListView) findViewById(R.id.my_list);
        new StudentPresenterOne(this).fetch();
    }

    @Override
    public void showLoading() {
        Toast.makeText(this, "正在加载数据中……", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showStudents(List<Student> students) {
        // 负责显示
        myList.setAdapter(new StudentAdapter(MainActivity.this,students));
    }
}

显示结果

这里写图片描述

通过MVP框架实现View改变

实现新的Model层

public class StudentModelImplTwo implements IStudentModel {
    @Override
    public void loadStudent(StudentOnLoadListener listener) {

        Log.i("castiel","执行了StudentModelImplTwo数据加载");
        // 模拟网络加载延时数据
        SystemClock.sleep(1000);

        //模拟Json数据
        List<Student> jsonStu2 = new ArrayList<Student>();
        jsonStu2.add(new Student("张三22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("李四22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("王五22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("赵六22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("陈七22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("孙八22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("猴子搬来的救兵22http://blog.csdn.net/mynameishuangshuai", R.mipmap.ic_launcher));

        // 通过回调方式传递数据
        if (listener != null) {
            listener.onComplete(jsonStu2);
        }
    }
}

实现新的Presenter层

public class StudentPresenterTwo {
    // Model
    IStudentModel mStudentModel = new StudentModelImplTwo();
    // View
    IStudentView mStudentView;
    // 初始化View
    public StudentPresenterTwo(IStudentView mStudentView) {
        this.mStudentView = mStudentView;
    }

    public void fetch(){
        mStudentView.showLoading();// 显示进度
        // Model获取数据
        if (mStudentModel != null){
            mStudentModel.loadStudent(new IStudentModel.StudentOnLoadListener() {
                @Override
                public void onComplete(List<Student> students) {
                    // 得到数据后给View显示数据
                    mStudentView.showStudents(students);
                }
            });
        }
    }
}

添加新的Activity,改变列表布局为网格布局

/**
 * 功能:TwoActivity 用网格形式实现
 * 作者:猴子搬来的救兵
 * 博客地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class TwoActivity extends AppCompatActivity implements IStudentView{

    GridView myGrid;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_two);
        myGrid = (GridView) findViewById(R.id.my_grid);
        new StudentPresenterTwo(this).fetch();
    }

    @Override
    public void showLoading() {
        Toast.makeText(this, "正在加载数据中……", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showStudents(List<Student> students) {
        // 负责显示
        myGrid.setAdapter(new StudentGridAdapter(TwoActivity.this,students));
    }
}

新的Adapter

public class StudentGridAdapter extends BaseAdapter {

    private LayoutInflater myInflater;
    private List<Student> data;

    public StudentGridAdapter(Context context, List<Student> data) {
        myInflater = LayoutInflater.from(context);
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View stuView = myInflater.inflate(R.layout.stu_item, null);
        Student student = data.get(position);
        ImageView imgStu = (ImageView) stuView.findViewById(R.id.img_stu);
        imgStu.setImageResource(student.getStuImg());
        TextView tvStu = (TextView) stuView.findViewById(R.id.tv_name);
        tvStu.setText(student.getName());
        return stuView;
    }

}

执行结果

这里写图片描述

作者:mynameishuangshuai 发表于2016/10/14 11:44:41 原文链接
阅读:409 评论:1 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>