美文网首页Android基础知识我爱编程
《第一行代码》---Android 啃完,学习笔记

《第一行代码》---Android 啃完,学习笔记

作者: Mythqian | 来源:发表于2016-09-15 00:45 被阅读616次

    #Android 基础知识点总结

    ----------

    ##1.adb - android debug bridge

    -adb start-server -----开启adb服务

    -adb kill-server -----停止adb服务

    -adb push 本地路径 手机路径 -----将文件导入手机

    -adb pull 手机路径 本地路径 -----导出

    -adb logcat -----查看Log

    -adb install [-r] [-s] 全路径/xxx.apk

    -r 重新安装该程序,保存数据

    -s 安装在SD卡内,而不是设备内部存储

    -adb uninstall [-k] 全路径/xxx.apk

    -k 不删除程序运行所产生的数据和缓存目录

    -adb remount -----重新挂载文件系统

    -adb reboot -----重启手机

    -adb reboot recovery -----重启到Recovery界面

    -adb reboot bootloader -----重启到bootloader界面

    -adb devices -----列出所有设备名称或IP

    -adb -s IP install 全路径-apk

    -adb shell -----挂载到Linux空间

    -netstat -ano ---查看进程 (协议  本地地址 外部地址  状态   PID)

    进入Linux空间后就好玩:

    ls ---查看当前路径下的文件

    ls -l ---查看文件的具体信息

    ps ---查看当前运行进程

    netstat -ano ---查看占用端口号的进程

    monkey 1000;---猴子测试 测试整个系统;1000代表数量

    monkey -p 包名 数量--- 测试某个应用程序运行数量后,会不会爆

    cd .. ---返回上级目录

    cat xxx.xml ---查看xml文件

    sqlite3 xxx.db ---进入后

    .help 查看帮助

    .tables 查看所有表名

    .quit 退出

    **可以使用sql语句操作表

    ##2.UI(脸蛋)

    1. 四种基本布局:

    #LinearLayout#---线性布局

    orientation:vertical竖直, horizontal 水平

    gravity:指定子布局位置

    layout_gravity:指定当前控件与父布局的相对位置

    layout_weight:出去已经分配的具体屏幕大小,将剩余的屏幕大小按权重分配(自己的理解)

    #RelativeLayout#---相对布局

    初始位置:在屏幕左上角

    容易忘记:layout_above="@id/xx"            在 xx控件上

    layout_below="@id/xx"            在 xx控件下

    layout_toRightOf="@id/xx"        在 xx控件右边

    layout_alignParentRight="true"    在父窗体右边

    layout_centerInParent="true"   在父窗体正中间

    #FrameLayout#---帧布局、

    特点:一层层覆盖

    不好玩,用来被替换的布局,当使用碎片fragment来布局UI的时候,framelayout里面存放一个fragment

    #TableLayout#---表格布局

    基本不用

    子控件 中无法指定宽度,使用 stretchColumns="1";

    合并单元格: layout_span="2";

    2. 常见控件:

    #TextView#

    textSize

    textColor    #ARGB  a:透明度 r:red g:green b:blue

    singleLine   true  单行显示

    maxLines     多行显示

    #Button#

    四种点击事件:

    第一种:通过给控件添加onclick属性,然后进到 activity中 去添加方法

    添加方法时,方法的签名也是固定的.(google不推荐使用)

    andorid:onclick="xxx"

    public void xxx(View v){

    }`

    第二种:给控件添加id , 然后 在activity中拿到 控件,然后 给控件添加onclick时间的监听器

    使用匿名内部类的写法:

    `android:id="@+id/xxx"

    xxx = (Button) findViewById(R.id.xxx);

    xxx.setOnClickListener(new OnClickListener(){

    public void onClick(View v){

    }

    });

    第三种:实际上与第二种一样, 只是换成了 内部类的写法.写个类去 实现 Onclick接口

    第四种:让activity类去实现 onclicklistner接口最终通过 switch...case去判断到底点击的是哪个控件.

    `android:id="@+id/xxx"

    xxx = (Button) findViewById(R.id.xxx);

    xxx.setOnClickListener(this);

    public void onClick(View v){

    int id = v.getId();

    switch(id){

    case R.id.xxx:

    break;

    default:

    break;

    }

    }

    #EditText#

    hint

    maxLines

    inputType

    et_content =(EditText) findViewById(R.id.et_content);

    String et_content = et_content.getText().toString.trim();

    if(TextUtils.isEmpty(et_content)){

    Toast.makeText("","",Toast.Length_SHORT).show();

    return;

    }

    #imageView#

    src

    ImageView.setImageResource(R.drawable.xxx);

    好玩的:实现图片轮播

    自己见一个xml文件:

    /animation-list>`

    'ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);

    rocketImage.setBackgroundResource(R.drawable.rocket_thrust);

    rocketAnimation = (AnimationDrawable) rocketImage.getBackground()

    ;

    rocketAnimation.start();

    #RadioGroup#

    eg:

    android:id="@+id/rg"

    android:layout_width="fill_parent"

    android:layout_height="wrap_content"

    android:orientation="horizontal">

    android:layout_width="0dip"

    android:layout_weight="1"

    android:layout_height="wrap_content"

    android:text="男"

    android:id="@+id/rb_male"

    />

    android:layout_width="0dip"

    android:layout_weight="1"

    android:layout_height="wrap_content"

    android:text="女"

    android:id="@+id/rb_female"

    />

    获取属性值:

    if(rg.getCheckedRadioButtonId() == R.id.rb_male){

    sex = "male";

    }else{

    sex = "female";

    }

    '

    #ProgressBar#

    max

    progress

    重要设置样式:

    style="?android:attr/progressBarStyleHorizontal"

    #VideoView#          播放音频视频,底层是SurfaceView 和 mediaPlayer的结合。

    vv = (VideoView) findViewById(R.id.vv);

    // 设置要

    vv.setVideoPath("/mnt/sdcard/lala.3gp");

    //mediaController --- 媒体控制器

    MediaController mc = new MediaController(this);

    mc.setAnchorView(vv);

    vv.setMediaController(mc);   // 成功的将MediaController与vv关联起来

    vv.start();

    #SurfaceView#

    sv = (SurfaceView) findViewById(R.id.sv);

    try {

    mPlayer = new MediaPlayer();

    mPlayer.reset();

    mPlayer.setDataSource("/mnt/sdcard/lala.3gp");

    mPlayer.prepare();

    } catch (Exception e) {

    e.printStackTrace();

    }

    sp = getSharedPreferences("config", 0);

    // surfaceHodler --- 界面的持有器,持有者

    sv.getHolder().addCallback(new Callback() {

    //surface销毁了

    @Override

    public void surfaceDestroyed(SurfaceHolder holder) {

    System.out.println("销毁了 ");

    Editor editor = sp.edit();

    editor.putInt("position", mPlayer.getCurrentPosition());

    editor.commit();

    mPlayer.stop();

    }

    //surface创建了

    @Override

    public void surfaceCreated(SurfaceHolder holder) {

    System.out.println("创建了  ");

    int position = sp.getInt("position", 0);

    mPlayer.setDisplay(sv.getHolder());    //显示画面,必须设置

    mPlayer.start();

    mPlayer.seekTo(position);  // 直接跳到某个位置, 从这个位置开始播放

    }

    //surface变化了

    @Override

    public void surfaceChanged(SurfaceHolder holder, int format, int width,

    int height) {

    System.out.println("修改了  ");

    }

    });

    #ScollView#  只能有一个子节点

    #WebView#

    webview = new WebView(this);

    //设置WebView属性,能够执行Javascript脚本

    webview.getSettings().setJavaScriptEnabled(true);

    //加载需要显示的网页

    webview.setWebViewClient(new WebViewClient());

    webview.loadUrl("http://www.xxxx.com/");

    //设置Web视图

    setContentView(webview);

    #Fragment#  碎片  类似Activity

    -1. 静态添加碎片

    1.自己布局一个fragment:

    android:id="@+id/fm_left"        //id必须写,不然会报错

    android:name="com.example.fragment.LeftFragment"// 添加name属性,包名必须写,此为加载LeftFragment 类

    android:layout_weight="1"

    android:layout_width="0dp"

    android:layout_height="match_parent"/>

    2.定义一个LeftFragment 继承 Fragment

    public class LeftFragment extends Fragment {

    @Override

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

    //false 代表 不附着在parent布局中

    return inflater.inflate(R.layout.left, container,false);

    }

    }

    3.定义一个R.layout.left布局文件

    -2. 动态添加碎片

    核心代码



    RightFragment rightFragment = new RightFragment();

    FragmentManager fragmentManager = getFragmentManager();

    FragmentTransaction beginTransaction = fragmentManager.beginTransaction();

    beginTransaction.replace(R.id.fl, rightFragment);

    beginTransaction.commit();

    3. 四种对话框(不能用Application的上下文)

    -1.取消对话框

    AlertDialog.Builder builder = new AlertDialog.Builder(this);

    builder.setTitle("please ");            // 下面都对话框都可以设置标题

    builder.setMessage("继续撸代码吗?");        // 同上

    builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {

    @Override

    public void onClick(DialogInterface dialog, int which) {

    Toast.makeText(MainActivity.this, "lu", 0).show();

    }

    });        //确定按钮的点击事件

    builder.setNegativeButton("No", null);    //取消按钮的点击事件

    builder.show();

    -2.单选对话框

    Builder builder = new AlertDialog.Builder(this);

    final String[] items = new String[]{"male","female"};

    builder.setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() {

    @Override

    //which 是数组的 索引,下同

    public void onClick(DialogInterface dialog, int which) {

    Toast.makeText(MainActivity.this, "你选的是"+items[which], 0).show();

    }

    });

    builder.show();

    -3.多选对话框

    AlertDialog.Builder builder = new AlertDialog.Builder(this);

    final String[] items = new String[]{"跳楼","上吊","撸代码","滚回家"};

    final boolean[] checkedItems = new boolean[]{false,false,false,false};

    // fasle 代表默认不被选中

    builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {

    @Override

    public void onClick(DialogInterface dialog, int which, boolean isChecked) {

    Toast.makeText(MainActivity.this, "你选的是"+items[which], 0).show();

    checkedItems[which] = isChecked;

    }

    });

    builder.show();

    -4.进度条对话框

    final ProgressDialog progressDialog = ProgressDialog.show(MainActivity.this, "please wait", "加载loading。。。。。");

    new Thread(){

    public void run(){

    progressDialog.setCancelable(false);//Back键不能取消掉

    SystemClock.sleep(2000);

    progressDialog.dismiss();            //设置 dismiss 退出

    }

    }.start();

    ***所有的控件都有的属性:visibility{visible(默认,可见),invisible(不可见占空间),gone(不可见也不占空间)}

    4. 自定义控件

    开源框架:SmartImageView(看源码)---- SmartImageView.setImageUrl(Url url){}

    1 . 继承原生控件

    2 . 重写构造方法

    3 . 根据业务需求定义方法

    5. ListView

    -1.作用和方法:用来将数据显示到屏幕上的技术

    google按照mvc的三层架构思想设计

    m:model--- 模型--- 需要显示的数据

    v:view ---视图---- 呈现的界面

    c:controller--- 控制器--- Adapter(适配器)

    1. lv.setSelection(random1); //该方法可以 ListView 定位到让指定位置(random1),就是让指定位置的条目位于当前界面第一行。非常准确。

    2. lv.smoothScrollToPosition(xxx); //该方法可以让 ListView 平滑的滚动到指定位置(xxx),但是非常不准确。

    3.  lv1.setOnScrollListener(OnScrollListener); //给ListView 设置监听器

    4. //滑动时回调的方法

    // scrollState 滚动状态有三种:0 静止,1 手指滑动,2惯性滚动

    @Override

    public void onScrollStateChanged(AbsListView view, int scrollState) { }

    @Override

    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}

    -2.BaseAdapter    ---是ListAdapter的一个默认实现类

    主要需要重写的2个方法:

    public int getCount(){        //总共需要显示数据条数

    return list.size();

    }

    public public View getView(int position, View convertView, ViewGroup parent) {

    //三种填充View的方法,本质是一样的

    //第一种

    View view = View.inflate(MainActivity.this,R,layout.item_main,null);

    //第二种

    View view = LayoutInflater.from(MainMainActivity.this).inflate(R,layout.item_main,null);

    //第三种

    LayoutInflater ll = (LayoutInflater)getSystemService("LAYOUT_INFLATER_SERVICE");

    View view = ll.inflate(R,layout.item_main,null);

    }

    -3.ArrayAdapter

    listView.setAdapter(new ArrayAdapter(MainActivity.this,R.layout.item,T[] Objects));

    最后一个参数也可以是集合。

    -4.SimpleAdapter

    待完善

    -5.优化

    (1)//删除之前已经显示的数据 ,然后再次重新加载进来,这样避免重复显示

    if(myadapter ==null){

    myadapter = new MyAdapter();

    lv.setAdapter(myadapter);

    }else{

    //要通知适配器去更新一下数据

    myadapter.notifyDataSetChanged();

    }

    (2)防止OOM(Out Of Memory)异常

    View view;

    if(convertView==null){

    view = View.inflate(MainActivity.this, R.layout.item, null);

    }else{

    view =convertView;

    }

    if(convertView==null){

    convertView = View.inflate(MainActivity.this, R.layout.item, null);

    }

    (3)faster(用Holder)持有器

    static class ViewHolder {

    TextView tv;

    ImageView iv;

    View Holder holder;

    if (convertView == null) {

    convertView=View.inflate(MainActivity.this,R.layout.item,null);

    holder = new ViewHolder();

    holder.iv = (ImageView) convertView.findViewById(R.id.iv);

    holder.tv_title = (TextView) convertView.findViewById(R.id.tv);

    convertView.setTag(holder);

    }else{

    holder = (ViewHolder) convertView.getTag();

    ##3.数据存储和解析

    1. 文件存储

    -1.基本输入输出流文件

    -2.Context提供了两个方法来打开数据文件里的文件IO流,存储路径:/data/data//files

    openFileInput("xxx.txt"); //读取

    openFileOutput("xxx.txt",Context.MODE_PRIVATE);    //保存

    //MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容

    //MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;

    //后面的两种模式在Android4.2被废弃,不安全

    //MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;

    //MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。

    deleteFile("xxx.txt");    //删除

    File getFilesDir():获取该应用程序的数据文件夹得绝对路径

    File getCacheDir():缓存区

    String[] fileList():返回该应用数据文件夹的全部文件

    -3.Sdcard 存储

    1、调用Environment的getExternalStorageState()方法判断手机上是否插了sd卡,且应用程序具有读写SD卡的权限,如下代码将返回true

    Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)

    2、调用Environment.getExternalStorageDirectory()方法来获取外部存储器,也就是SD卡的目录,或者使用"/mnt/sdcard/"目录

    File file = new File(Environment.getExternalStorageDirectory(),filename);

    3、使用IO流操作SD卡上的文件

    注意点:

    手机应该已插入SD卡,对于模拟器而言,可通过mksdcard命令来创建虚拟存储卡

    必须在AndroidManifest.xml上配置读写SD卡的权限

    2. SharedPreferences存储 --- 单例,一般不会发生并发冲突

    1.创建文件:    SharedPreferences sp = context.getSharedPreferences("xxx",int mode);//创建的xxx一定是xml文件,存储在/data/data//shared_prefs,参数mode 和上面一样。

    2.存储数据:    Editor edit = sp.edit();

    edit.putXxx(String key,Xxx value);

    edit.commit(); || edit.apply();

    区别: 1. apply没有返回值apply方法不会提示任何失败的提示。而commit返回boolean表明修改是否提交成功

    2. apply异步提交数据,commit同步。

    3.读取数据: sp.getXxx(String key,XxxDefault);// 如果没有读取到,就返回第二参数。

    4.删除数据: sp.remove(String key);//删除key字段

    sp.clear();//清空文件

    3. SQLite数据库存储

    -1.创建表

    db.execSQL("create table tablename(_id integer primary key autoincrement,name varchar(30),age Integer)");

    -2.Insert

    第一种:存在sql注入问题

    db.execSQL("insert into tablename(name,age) values('Myth',2)");

    第二种:通过占位符? 解决sql注入

    db.execSQL("insert into tablename(name,age) values(?,?)",new Object[]{"Myth",2});

    第三种:底层还是通过拼接字符串得到sql语句

    ContentValues values = new ContentValues();

    values.put("name","Myth");

    values.put("age",2);

    db.insert("tablename",columnNull,values);//第二个参数:sql表不允许插入空,所以就会用null当作该值插入表。

    -3.Delete

    db.execSQL("delete from tablename where _id = 2");

    db.execSQL("delete from tablename where _id = ?",new Object[]{2});

    db.delete("tablename","_id = ?",new String[]{String.valueOf(id)});

    -4.Update

    db.execSQL("update tablename set name = 'Myth' where age = 2");

    db.execSQL("update tablename set name=?,age =? where _id=?",new Objecet[]{"Myth",2,2});

    ContentValues values = new ContentValues();

    values.put("name","Myth");

    values.put("age",2);

    db.update("tablename",values,"_id=?",new String[]{String.valueOf(id)});

    -5.Query

    Cursor cursor = db.rawQuery("select * from tablename where _id=2");

    Cursor cursor = db.rawQuery("select * from tablename where _id=?",new Object[]{2});

    Cursor cursor = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);

    if(cursor.moveToFirst){

    while(cursor.moveToNext){

    }

    }

    4. SQLite实践

    -1.事务操作  --- 同时处理多条数据时,保证数据安全性

    db.beginTransaction();

    try {

    db.setTransactionSuccessful();//执行提交

    } finally {

    db.endTransaction();

    }

    -2.数据库升级操作 --- 保证版本更新成功

    switch (oldVersion) {

    case 1:        //代表每个版本的操作 该case 是1~2的升级操作

    case 2:        //case 一直到最新版本

    default:

    }

    -3.注意:getWritableDatabase()和getReadableDatabase()方法的区别:

    getWriteableDatabase()方法以读写方式打开数据库一旦数据库的磁盘空间满了,数据库就只能读而不能写,倘若使用getWritableDatabase()打开数据库就会出错。

    getReadableDatabase()方法先以读写方式打开数据库,如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读的方式打开数据库.

    5. XML生成器:XmlSerializer和 XML解析器:XmlPullParser

    -1.XmlSerializer:(字符串拼接Xm文件存在语句注入问题,so用XmlSerializer)

    XmlSerializer serializer = Xml.newSerializer();

    serializer.setOutput(fos,"UTF-8");

    serializer.startDocument("UTF-8",true);// "

    serializer.startTag(null, "smses");

    for (int i = 0; i < 50; i++) {

    serializer.startTag(null, "sms"); //第一个参数为命名空间

    serializer.startTag(null, "body");

    serializer.text("我是内容<>" + i);

    serializer.endTag(null, "body");

    serializer.endTag(null, "sms");

    }

    serializer.endTag(null, "smses");

    serializer.endDocument;

    fos.close;

    -2.XmlPullParser:

    XmlPullParser pullparser = Xml.newPullParser();

    pullparser.setInput(fis,"UTF-8");

    int eventType = pullparser.next();

    while(eventType != XmlPullParser.END_DOCUMENT){

    switch(eventType){

    case XmlPullParser.START_TAG:

    if ("smses".equals(tagName)) {

    list = new ArrayList();

    }else if ("sms".equals(tagName)) {

    sms = new Sms();

    }else if ("body".equals(tagName)) {

    sms.setBody(parser.nextText());

    }

    break;

    case XmlPullParser.END_TAG:

    if ("sms".equals(tagName)) {

    list.add(sms);

    }

    break;

    }

    eventType = pullparser.next();

    }

    ##4.Activity

    -1.自定义的Activity

    -1.创建一个MyActivtiy实现Activity

    public class MyActivity extends Activity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    }

    -2.新建一个布局文件 xxx.xml

    -3.在onCreate()方法里写 上 setContentView(R.layout.xxx);

    -4.最重要的一步:在清单文件中注册一个自己的Activity

    android:name="com.example.MyActivity"    //可以缩写.MyActivity

    android:label="@string/app_name" >        //标题栏的内容

    -5.非必须的一步:隐藏标题栏 ---在onCreate()方法内写

    requestWindowFeature(Window.FEATURE_NO_TITLE);

    -2.生命周期

    OnCreate()        创建界面

    OnStart()        不可见---->可见  时候会调用

    OnResume()        获得焦点,此时活动在栈顶,处于运行状态

    OnPause()        失去焦点,不在栈顶但可见,处于暂停状态

    OnStop()        可见---->不可见  时候会调用,处于停止状态

    OnDestroy()        销毁

    **注意:要使得Activity不可见,可以再建一个项目同时运行,将其Application的theme属性改成透明状态:

    android:theme="@android:style/Theme.Translucent">

    -3.活动的四种启动模式(android:launchMode="")

    -1.standard:每次启动acitvity组件时, 都会新创建activity 实例,是活动默认的启动模式

    -2.singleTop:如果某个activity设置单一顶部模式, 那么当发现当前的activity就在当前任务栈的顶部, 那么就不再新创建当前的activity的实例.例如:系统的短信

    -3.singleTask:如果当前任务栈中已经有当前activity 的实例, 那么就将当前activity 的实例直接拿过来用, 用的时候,如果当前activity 不在栈顶,那么将在当前activity 之上的其他的activity的实例 给干掉... 然后再处于栈顶了一般情况下, 当某个activity  启动的时候, 要占用的内存比较大, 而手机上的内存又是有限的, 那么这个时候, 就推荐将这个activity的启动模式设置为单一任务栈模式.例如:系统的浏览器

    -4.singleInstance:如果某个activity 的启动模式设置为单一实例模式, 那么系统会为这个activity 单独的去开辟一个任务栈,这个任务栈中,只放这个 activity的实例.这样确保了整个操作系统中,只有一个这个activity的实例了.如果某个activity在整个系统中就只需要有一个实例, 并且永远不会更改, 那么就推荐使用这种模式.例如:系统的电话

    -4.使用intent在活动之间传输数据

    -1.显示意图:是明显的指定要激活哪个组件 ..一般建议用在应用程序内部 .

    Intent intent = new Intent();

    //    intent.setClass(this, SecondActivity.class);

    //    intent.setClassName("com.itheima.exactintent", "com.itheima.exactintent.SecondActivity");

    intent.setClassName(this,  "com.itheima.exactintent.SecondActivity");

    startActivity(intent);

    -2.隐式意图:是指不明确到底哪个组件可以响应你的意图,你只需要将意图发出去就可以了.当存在满足你的意图的组件时,

    这个时候,系统就会将组件给激活起来...一般用在不同应用程序之间 激活组件 ..

    Intent intent = new Intent();

    //必须和Activity下的intent-filter内容匹配

    intent.setAction("com.itheima.nu");

    //    intent.addCategory("android.intent.category.DEFAULT");

    intent.addCategory(Intent.CATEGORY_DEFAULT);

    startActivity(intent);

    ****1. 每个应用中的组件是可以配置多个Intent-filter,可以配置多个隐式意图去激活这个组件

    2. 如果想要激活这个组件, 只需要发送对应的隐式意图就可以了.

    -3.intent中setData:接收一个Uri对象,例如:intent.setData(Uri.parse("www.baidu.com"));

    与其对应,可以在标签中配置一个子标签。

    属性:scheme(http),host(www.baidu.com),port(8080),path(指定端口号后面的部分),mimeType(指定可以处理的数据类型,允许使用通配符)

    -4.intent携带数据,实现短信分享:

    Intent intent = new Intent();

    //短信有很多intent-filter,只需要其中匹配上一个即可

    intent.setAction("android.intent.action.SENDTO");

    intent.addCategory("android.intent.category.DEFAULT");

    intent.addCategory("android.intent.category.BROWSABLE");

    intent.setData(Uri.parse("smsto://xxxxx"));

    intent.putExtra("sms_body", "xxxxxxxxxx");

    startActivity(intent);

    //Activity接收数据

    Intent intent = getIntent();

    String data = intent.getExtraString("sms_body");

    -5.带有返回结果的intent

    主Activity:

    Intent intent2 = new Intent();

    intent2.setClass(this, xxx.class);

    startActivityForResult(intent2, 2);

    //接收其他Activity回传的结果数据

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

    System.out.println("结果返回到这里了 ....");

    if(requestCode==1){

    }else if(requestCode==2){

    }

    super.onActivityResult(requestCode, resultCode, data);

    }

    xxxActivity:

    Intent data = new Intent();

    data.putExtra("contact", contact);

    setResult(0, data);

    finish();        // 关闭该xxxActivity

    -6.其他

    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);重新启动Activity

    intent.addAction("android.media.action.IMAGE_CAPTURE");启动相机

    ##5.BroadcastReceiver(买收音机,装电池,调频道)

    -1.动态注册广播接收者

    (1)监听手机屏幕开关

    receiver = new ScreenStateReceiver();//继承BroadcastReceiver

    IntentFilter filter = new IntentFilter();

    filter.addAction("android.intent.action.SCREEN_OFF");

    filter.addAction("android.intent.action.SCREEN_ON");

    registerReceiver(receiver, filter);//注册 广播接收者

    public void onReceive(Context context, Intent intent) {

    String action = intent.getAction();

    if("android.intent.action.SCREEN_OFF.equals(action)){

    System.out.println("发现用户屏幕关了  ");

    abortBroadcast();//拦截广播

    }else if("android.intent.action.SCREEN_ON".equals(action)){

    System.out.println("发现用户屏幕开启 ");

    }

    }

    (2)监听网络变化

    receiver = new NetworkChangeReceiver();//继承BroadcastReceiver

    IntentFilter filter = new IntentFilter();

    filter.addAction("android.intent.action.CONNECTIVITY_CHANGE");

    registerReceiver(receiver, filter);//注册 广播接收者

    (3)需要手动取消注册

    在onDestroy()中调用 unregisterReceiver(receiver);

    -2.静态注册一个广播接收者 --- 监听开机启动

    //优先级-1000~10000,优先接收广播

    -3.发送无序广播

    // 定义 intent , intent 设置 必要的信息就可以 了

    Intent intent = new Intent();

    intent.setAction("com.example.xxx");

    // 发送 广播

    sendBroadcast(intent);

    -4.发送有序广播

    // 定义 intent , intent 设置 必要的信息就可以 了

    Intent intent = new Intent();

    intent.setAction("com.example.xxx");

    // 发送 广播

    sendOrderBroadcast(intent,null);//第二个参数是一个与权限有关的字符串

    ##6.Server

    -1.Handler异步消息处理机制(IPC 线程通信,避免发生ANR 问题,在Activity中 耗时操作 5s 产生ANR。)

    -1.Message:线程间传递消息,

    //获得消息对象,两种方式本质一样

    Message msg = handler.obtainMessage();

    Message msg = Message.obtain();

    msg.sendToTarget(); //该msg需要绑定handler,从handler.obtainMessage()获取

    -2.Handler:用于发送和处理消息

    handler.sendMessage();

    handler.sendEmptyMessage();//发送无效消息

    private Handler handler = new Handler(){

    public void handleMessage(android.os.Message msg) {

    switch (msg.what) {

    case SUCCESS:

    Xxx data = (Xxx)msg.obj;

    Toast.makeText(MainActivity.this, "成功", 0).show();

    break;

    case FAILURE:

    Toast.makeText(MainActivity.this, "失败", 0).show();

    break;

    }

    }

    };

    -3.MessageQueue :消息队列,存放将要被处理的消息,每个线程只有一个MessageQueue

    -4.Looper:调用loop(),进入一个无线循环中,不断取出MessageQueue中的消息传递到handleMessage()中,每个线程也只有一个Looper对象。

    -5.发送消息代码

    Message msg = Message.obtain();

    msg.what = SUCCESS;        // what 区分发送的消息类型

    msg.obj = data;            // obj 携带任意消息

    handler.sendMessage(msg);

    -2.AsyncTask ---Android已经封装好,方便在子线程中对UI进行操作。(待更新)

    -3.进程优先级分类:

    -1.前台进程: 就是正在与用户进程交互的引用程序

    -2.可视进程: 用户看得见的,但是摸不着的

    -3.服务进程: 在服务中运行, 在后台运行着

    -4.后台进程: 在后台一直运行着,不是运行在service ,是运行在activity中

    -5.空进程:  引用程序已经退出了,没有activity,没有service .

    -6.总结:前台进程> 可视进程> 服务进程> 后台进程> 空进程   当系统内存不够用的时候, 就会去尝试回收进程,来重新分配内存. 会按照如上优先级分类杀掉进程.

    -4.自定义服务

    public class MyService extends Service {

    @Override

    public IBinder onBind(Intent intent) {

    return new MyBinder();

    }

    }

    // 必须清单文件注册

    -5.开启服务的生命周期

    onCreate()            //服务创建时调用

    onStartComment()    //每次服务启动时调用,废弃了onStart()

    onDestroy()            //服务销毁时调用

    -1.开启服务

    Intent intent = new Intent();

    intent.setClass(MainActivity.this,MyService.class);

    startService(intent);

    -2.停止服务

    Intent intent = new Intent();

    intent.setClass(MainActivity.this,MyService.class);

    stopService(intent);

    -6.绑定服务的生命周期

    onCreate()        //创建服务

    onBind()         //绑定服务

    onUnbind()         //解除绑定服务

    onDestroy()     //销毁服务

    (1)绑定服务时, onStartCommand并不会执行,

    (2)绑定服务,服务何时销毁呢, 在应用程序退出的时候就销毁了

    (3)绑定服务, 服务不会在后台运行 ---在apps中running 中是看不到的

    (4)开启服务, 服务会在后台运行--- 可以在apps中 runnning 中看到

    (5)绑定服务, 服务只会创建一次, 如果再次绑定服务, 那么服务是不会重新再次创建的.

    (6)如果没有绑定服务, 就直接去解绑服务, 那么会抛异常.so 需要做如下判断,避免异常

    if(conn != null){

    unbindService(conn);

    conn = null;

    mybinder = null;

    }

    -7.绑定服务实现流程(活动和服务间通信,进程间通信使用 IBinder)

    -1.绑定服务

    Intent intent = new Intent();

    intent.setClass(this, MyService.class);

    if(conn == null){

    conn = new MyConnection();

    }

    bindService(intent, new MyConnection(), BIND_AUTO_CREATE);

    -2.编写MyBinder 继承 Binder 实现 IService  // IService 接口中是将要调用的方法

    private class MyBinder extends Binder implements IService{

    @Override

    // 服务中对外提供的方法封装在接口中,重写接口中的方法

    public void call(String name, String service) {

    calle(name, service);

    }

    //该方法为不对外提供的方法,只对内访问

    public void callee(String name, String service) {

    System.out.println("呼叫前台");

    }

    }

    -3.向Activity传回MyBinder对象

    @Override

    public IBinder onBind(Intent intent) {

    return new MyBinder();

    }

    -4.实现MyConnection 实现 ServiceConnection接口

    private class MyConnection implements ServiceConnection{

    @Override

    //当与服务建立联系的时候被回调

    public void onServiceConnected(ComponentName name, IBinder service) {

    if(mybinder == null){

    mybinder = (IService) service;

    //    mybinder = IService.Stub.asInterface(service);

    }

    }

    @Override

    //当与服务断开联系的时候被回调

    public void onServiceDisconnected(ComponentName name) {

    mybinder = null;

    }

    }

    -5.调用服务中的方法

    mybinder.call("xxxx", "xxxx");

    -8.不同Activity之间通信

    主要编写.aidl

    (1)语法与接口很类似,直接修改接口文件后缀名即可。但是.aidl文件中不能写权限修饰符

    (2).aidl文件接受参数和返回值类型是8种基本数据类型,如果是引用数据类型,必须实现Parceable接口

    (3)不能修改.aidl文件生成的.java类

    -1 Parceable 比 Serializable 性能高

    -2 Serializable 使用时会产生大量临时变量,引起频繁的GC

    -3 Parceable 不能保证数据的持续性,不能使用数据保存在磁盘上

    -9.混合开发服务

    (1)开启服务:只能在后台运行

    (2)绑定服务:只能调用服务中的方法

    想在后台运行,但是同时又想 调用服务中 方法, 那么 就需要混合开启服务了.

    以后,如果需要在后台运行, 并且又需要调用服务中的方法时,请严格按照 如下的顺序去 实现程序的逻辑, 否则 就容易出现问题..

    混合开启:

    开启服务

    绑定服务

    调用服务中的方法

    解除绑定

    停止服务

    例如:QQ 在退出的时候,选择退出后, 仍然可以接受消息, 就可以使用混合开启服务的方式...

    -10.Android提供一个IntentService类:是一个异步的,会自动停止的服务,可以避免ANR问题(服务在主线程中耗时操作 10s 产生ANR 问题)

    public class MyIntentService extends IntentService {

    public MyIntentService() {

    super("MyIntentService");

    }

    @Override

    protected void onHandleIntent(Intent intent) {

    }        //处理具体的逻辑

    }

    // 清单文件注册

    ##7.ContentProvider

    -1.自定义内容提供者  ---ContentProvider

    public class MyContentProvider extends ContentProvider {

    private static final int SUCCESS = 0;

    private static UriMatcher matcher;

    //匹配器 -- UriMatcher

    //是固定的写法

    //这里的no_match是用来指定当匹配不成功时,返回的值

    static {

    matcher = new UriMatcher(UriMatcher.NO_MATCH);

    // com.itheima.xxx 公开的名称, table就是暗号了, SUCCESS指匹配成功后返回的结果

    matcher.addURI("com.example.xxx", "table", SUCCESS);

    }

    @Override

    public boolean onCreate() {

    return false;

    }

    @Override

    public Cursor query(Uri uri, String[] projection, String selection,

    String[] selectionArgs, String sortOrder) {

    BankOpenHelper myOpenHelper = new MyOpenHelper(getContext());

    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    if (matcher.match(uri) == SUCCESS) {

    return db.query("table", projection, selection, selectionArgs, null,

    null, sortOrder);

    }

    return null;

    }

    @Override

    public String getType(Uri uri) {

    return null;

    }

    @Override

    public Uri insert(Uri uri, ContentValues values) {

    BankOpenHelper myOpenHelper = new MyOpenHelper(getContext());

    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    // TODO Auto-generated method stub

    if (matcher.match(uri) == SUCCESS) {

    db.insert("table", null, values);

    } else {

    try {

    throw new Exception("暗号错误。");

    } catch (Exception e) {

    e.printStackTrace();

    }

    }

    return null;

    }

    @Override

    public int delete(Uri uri, String selection, String[] selectionArgs) {

    BankOpenHelper myOpenHelper = new MyOpenHelper(getContext());

    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    if (matcher.match(uri) == SUCCESS) {

    return db.delete("account", selection, selectionArgs);

    }

    return 0;

    }

    @Override

    public int update(Uri uri, ContentValues values, String selection,

    String[] selectionArgs) {

    BankOpenHelper myOpenHelper = new MyOpenHelper(getContext());

    SQLiteDatabase db = myOpenHelper.getWritableDatabase();

    if (matcher.match(uri) == SUCCESS) {

    return db.update("account", values, selection, selectionArgs);

    }

    return 0;

    }

    }

    ##清单文件注册:

    -2. 内容解析者 ---- ContentResolver (类似SQLite操作)

    1 insert

    ContentResolver resolver = getContentResolver();

    Uri url = Uri.parse("content://com.example.xxx/table");

    ContentValues values = new ContentValues();

    values.put("key", "value");

    resolver.insert(url, values);

    2.delete

    ContentResolver resolver = getContentResolver();

    Uri url = Uri.parse("content://com.example.xxx/table");

    resolver.delete(url, "key=?", new String[]{"value"});

    3.update

    ContentResolver resolver = getContentResolver();

    Uri url = Uri.parse("content://com.example.xxx/table");

    ContentValues values = new ContentValues();

    values.put("key1", "value1");

    resolver.update(url, values, "key=?", new String[]{"value"});

    4.query

    ContentResolver resolver = getContentResolver();

    Uri url = Uri.parse("content://com.example.xxx/table");

    Cursor cursor = resolver.query(url, null, "key=?", new String[]{"value"}, null);

    while(cursor.moveToNext()){

    }

    -3.内容观察者 --- ContentObserver

    (1)ContentResolver resolver = getContentResolver();

    //注册一个内容观察者

    //如果notifyForDescendents参数设为true,假如Uri为content://abc,那么Uri为content://abc/xyz, content://abc/xyz/foo的数据改变时也会触发该监听器,如果参数为false,那么只有content://abc的数据改变时会触发该监听器

    resolver.registerContentObserver(uri, boolean notifyForDescendents, new ContentObserver(null) {

    // selfChange : 数据的变化是否是来自于自己

    @Override

    public void onChange(boolean selfChange) {

    System.out.println("数据发生变化了 ....");

    }

    });

    (2)在不需要时,需要手动的调用

    unregisterContentObserver()去取消注册。

    (3)ContentObserver类介绍

    构造方法 public void ContentObserver(Handler handler) {}

    说明:所有 ContentObserver的派生类都需要调用该构造方法

    参数: handler  Handler对象。可以是主线程Handler(这时候可以更新UI了),也可以是任何Handler对象。

    ##8.手机多媒体

    -1.通知的使用(NotificationManager)

    -1.第一种

    //拿到通知管理器

    NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

    Notification nf = new Notification(R.drawable.ic_launcher,"This is ticker Text ",System.currentTimeMillis());

    // 第四个参数为延期意图(能跳转干你想干的事)

    nf.setLatestEventInfo(context, "This is content title", "This is content text", null);

    //显示通知,第一个参数是每一个通知的id(保证不同)

    nm.notify(1,nf);

    -2.第二种

    //基于Notification是builer模式构建,可以链式编程创建

    Notification nf = new Notification.Builder(this)

    .setContentTitle("你有消息")            //标题

    .setContentText("你mama 喊你回家吃饭")    //文本

    .setSmallIcon(R.drawable.ic_launcher)    //小图标

    .setLargeIcon(BitmapFactory.decodeResource(getResources(),

    R.drawable.ic_launcher))     //大图标

    .build();

    NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

    nm.notify(1,nf);

    -3.通过pending(延期意图)拨打电话

    Notification nf = new Notification(R.drawable.ic_launcher, "你好一有消息", System.currentTimeMillis());

    Intent intent = new Intent();

    intent.setAction(Intent.ACTION_CALL);

    intent.setData(Uri.parse("tel:"+5556));

    // pendingIntent 可以getActivity()  getService()  getBroadcast()

    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

    nf.setLatestEventInfo(this, "ai", "huijiachifan", pendingIntent);

    //权限不能少,打电话权限

    -2.短信的接收和发送(SmsManger)

    -1.接收短信:监听 android.provider.Telephony.SMS_RECEIVED的广播

    public class MessageReceiver extends BroadcastReceiver {

    @Override

    public void onReceive(Context context, Intent intent) {

    Bundle bundle = intent.getExtras();

    //通过密钥"pdus"获取SMS pdu 数组,每一个pdu是一条短信

    Object[] objects = (Object[])bundle.get("pdus");

    for (Object object : objects) {

    //将每一个pdu字节数组转换SmsMessage对象

    SmsMessage smsMessage = SmsMessage.createFromPdu((byte[])object);

    //获取短信的发送方号码

    if(smsMessage.getOriginatingAddress().equals("55666")){

    //获取短信

    smsMessage.getMessageBody();

    }

    }

    }

    }

    //接收短信权限

    -2.发送短信

    SmsManager smsManager = SmsManager.getDefault();

    //发送很长很长的短信需要切割短信存到集合中,才能一条一次性发出去

    ArrayList divideMessage = smsManager.divideMessage("");

    smsManager.sendMultipartTextMessage("phone", null, divideMessage, null, null);

    //接收短信权限

    -3.监听手机状态(TelephonyManager):开启录音功能(MediaRecoder)

    TelephonyManager manager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);

    manager.listen(new myPhoneStateListener(), PhoneStateListener.LISTEN_CALL_STATE);

    private class myPhoneStateListener extends PhoneStateListener{

    @Override

    public void onCallStateChanged(int state, String incomingNumber) {

    // TODO Auto-generated method stub

    super.onCallStateChanged(state, incomingNumber);

    switch (state) {

    if(mRecorder != null){

    stopRecording();

    }

    break;

    case TelephonyManager.CALL_STATE_OFFHOOK:

    startRecording();

    break;

    case TelephonyManager.CALL_STATE_RINGING:

    break;

    default:

    break;

    }

    }

    }

    MediaRecorder mRecorder;

    private void startRecording() {

    //下面是模版,可以录音文件的格式有:3gp,mp3。。。

    mRecorder = new MediaRecorder();

    mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);

    mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);

    mRecorder.setOutputFile("/mnt/sdcard/jianting.3gp");

    mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

    try {

    mRecorder.prepare();

    } catch (IOException e) {

    //    Log.e(LOG_TAG, "prepare() failed");

    }

    mRecorder.start();

    }

    private void stopRecording() {

    mRecorder.stop();

    mRecorder.release();

    mRecorder = null;

    }

    //录音权限,读手机状态权限

    -4 播放音视频(MediaPlayer)

    setDataSource() // 设置要播放

    seekTo()//    从指定位置播放

    isPlaying()//当前是否播放

    getDuration// 获取音频时长

    // 开始播放Button

    public void start(View v){

    //播放的地址

    String dizhi = et_shu.getText().toString().trim();

    if(TextUtils.isEmpty(dizhi)){

    Toast.makeText(this, "不能为空", Toast.LENGTH_SHORT).show();

    return;

    }

    player = new MediaPlayer();

    player.reset();

    try {

    player.setDataSource(dizhi);

    player.prepare();

    //异步准备

    /*player.prepareAsync();

    player.setOnPreparedListener(new OnPreparedListener() {

    @Override

    public void onPrepared(MediaPlayer mp) {

    // TODO Auto-generated method stub

    player.start();

    }

    });*/

    player.start();

    } catch (Exception e) {

    e.printStackTrace();

    }

    }

    // 暂停Button

    public void pause(View v){

    if(player!= null && player.isPlaying()){

    player.pause();

    return;

    }

    if(player!= null){

    player.start();

    }

    }

    // 停止Button

    public void stop(View v){

    if(player!= null){

    player.stop();

    player.release();

    player = null;

    }

    }

    -5.传感器的使用(SensorManager)

    SensorManager sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

    // Sensor.TYPE_XXX 可以获取

    Sensor lightSensor = sm.getDefaultSensor(Sensor.TYPE_XXX);

    MySensorEventListener myListenser = new MySensorEventListener();

    // 必须注册监听事件,第二个参数 是Senor实例,第三个参数是 传感器输出信息的更新速率

    有4个值:SENSOR_DELAY_UI  SENSOR_DELAY_NORMAL  SENSOR_DELAY_GAME  SENSOR_DELAY_FASTEST   越来越快,耗电

    sm.registerListener(myListenser, XXXSensor, SensorManager.SENSOR_DELAY_NORMAL);

    }

    // 不使用的时候取消监听,省电

    @Override

    protected void onDestroy() {

    super.onDestroy();

    if(myListenser!=null){

    sm.unregisterListener(myListenser);

    }

    }

    private class MySensorEventListener implements SensorEventListener{

    // sensor获得周围环境数据发生变化时 ,会被调用

    @Override

    public void onSensorChanged(SensorEvent event) {

    //编写逻辑事件

    }

    // sensor获得周围环境数据发生变化时,精确度发生变化时会被调用

    @Override

    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }

    }

    // 获取Sensor.TYPE_ALL

    List list = sm.getSensorList(Sensor.TYPE_ALL);

    System.out.println("数量 : " + list.size());

    for (Sensor sensor : list) {

    System.out.println(sensor.getName() +" , 类型 值 : " + sensor.getType());

    }

    ##9.网络编程

    -1.HttpURLConnection

    1.get请求(拼接URL。。)

    //防止乱码  URL编码

    String username = URLEncoder.encode(username, "UTF-8");

    String password = URLEncoder.encode(password, "UTF-8");

    URL url = new URL(path+"?username="+username+"&password="+password);

    HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    conn.setRequestMethod("GET");

    //伪装设置成windows端查看网页

    //conn.setRequestProperty("User-Agent",

    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586");

    //设置连接超时时间

    conn.setConnectTimeout(5000);

    //获取响应码

    int code = conn.getResponseCode();

    2.post请求

    URL url = new URL(path);

    String params = "?username="+URLEncoder.encode(username, "UTF-8")+"&password="+URLEncoder.encode(password, "UTF-8");

    HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    conn.setRequestMethod("POST");

    //设置连接超时时间

    conn.setConnectTimeout(5000);

    //获取响应码

    conn.setDoOutput(true);

    conn.getOutputStream().write(params.getBytes());

    int code = conn.getResponseCode();

    -2.HttpClient(Apache) ---android 6.0 废弃

    1.get请求

    username = URLEncoder.encode(username, "UTF-8");

    String path = "http://169.254.26.249:8080/qqlogin/servlet/login?username="+username+"&password="+password;

    DefaultHttpClient client = new DefaultHttpClient();

    HttpGet get = new HttpGet(path);

    //执行请求,拿到响应对象

    HttpResponse response = client.execute(get);

    int code = response.getStatusLine().getStatusCode();

    if(code == 200){

    //拿到服务端响应的输入流

    InputStream is = response.getEntity().getContent();

    String login = StreamTool.decodeStream(is);

    2.post请求

    String path = "http://169.254.26.249:8080/qqlogin/servlet/login";

    DefaultHttpClient client = new DefaultHttpClient();

    HttpPost post = new HttpPost(path);

    //通过一个NameValuePair集合来存放待提交的数据

    List list = new ArrayList();

    list.add(new BasicNameValuePair("username", username));

    list.add(new BasicNameValuePair("password", password));

    //防止乱码

    post.setEntity(new UrlEncodedFormEntity(list , "UTF-8"));

    //执行请求,拿到响应对象

    HttpResponse response = client.execute(post);

    int code = response.getStatusLine().getStatusCode();

    if(code == 200){

    InputStream is = response.getEntity().getContent();

    String login = StreamTool.decodeStream(is);

    -3.AsyncHttpClient---post请求

    AsyncHttpClient client = new AsyncHttpClient();

    RequestParams params = new RequestParams();

    params.add("username", username);

    params.add("password", password);

    client.post(path, params, new AsyncHttpResponseHandler() {

    @Override

    //statusCode 是 响应码        headers响应头   responseBody 响应体

    public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {

    Toast.makeText(MainActivity.this,"登录结果为"+new String(responseBody), 0).show();

    }

    @Override

    public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {

    Toast.makeText(MainActivity.this,"登录结果为"+new String(responseBody), 0).show();

    }

    相关文章

      网友评论

      • 13408e285093:你好,我现在刚看到第二章,讲activity生命周期那个例子,照书上抄的代码,不知道为什么点击第二个按钮程序就停止了,请问你当时遇到过这个问题吗?希望帮忙解惑,谢谢!
        Mythqian:我。。。抱歉。。。一年。。了

      本文标题:《第一行代码》---Android 啃完,学习笔记

      本文链接:https://www.haomeiwen.com/subject/rrcrettx.html